Interactive Tutorial: Your First Tunnel
Overview
This document describes the implementation details for the "Your First Tunnel" interactive tutorial feature. The tutorial guides new users through creating their first Jetty tunnel with step-by-step validation, UI highlights, and real-time progress tracking.
Purpose
The interactive tutorial system provides:
- Guided onboarding for new users unfamiliar with the CLI workflow
- Progressive disclosure of features and concepts
- Real-time validation that users complete each step correctly
- Contextual help and troubleshooting at each stage
- Gamification through badges, points, and celebrations
Architecture
Components
The tutorial system consists of four main components:
- Tutorial Definition (
your-first-tunnel.json) - Declarative JSON structure defining steps, validation, and UI behavior - Tutorial Engine (Frontend) - React/Vue component that renders steps and manages state
- Validation API (Backend) - Laravel endpoints that verify step completion
- UI Highlighting (Frontend) - DOM manipulation to highlight specific elements
Data Flow
User Action → Frontend State Update → Backend Validation → Progress Update → Next Step
JSON Schema Reference
Root Structure
{
"id": "string", // Unique tutorial identifier
"title": "string", // Display title
"description": "string", // Short description for listings
"category": "string", // Category (onboarding, advanced, etc.)
"duration": number, // Estimated minutes
"difficulty": "string", // beginner|intermediate|advanced
"tags": ["string"], // Search/filter tags
"prerequisites": ["string"], // Required prerequisites
"learningObjectives": ["string"],
"completionRewards": {...},
"steps": [...],
"summary": {...},
"metadata": {...}
}
Step Structure
Each step in the steps array:
{
"id": "string", // Unique step ID
"title": "string", // Step title
"description": "string", // Short description
"content": "string", // Markdown content body
"icon": "string", // Icon name (heroicons, fontawesome, etc.)
"codeExample": {...}, // Optional code snippet
"validation": {...}, // How to verify completion
"helpText": "string", // Markdown help content
"tips": ["string"], // Quick tips array
"troubleshooting": [...], // Common issues and solutions
"nextAction": "string", // Clear call-to-action
"nextButton": "string", // Button text
"skipAllowed": boolean, // Can user skip this step?
"celebration": {...} // Animation/message on completion
}
Validation Types
Manual Validation
User manually confirms completion (no automatic check):
{
"type": "manual",
"message": "Ready to proceed"
}
API-Based Validation
Backend endpoint checks completion status:
{
"type": "api_token_exists",
"message": " Token created successfully!",
"endpoint": "/api/tutorial/validate/token-exists",
"pollInterval": 2000, // Poll every 2 seconds
"timeout": 300000 // 5 minute timeout
}
Conditional Validation
Multiple conditions must be met:
{
"type": "multiple_requests",
"message": " Multiple requests logged!",
"endpoint": "/api/tutorial/validate/multiple-requests",
"pollInterval": 3000,
"timeout": 300000,
"criteria": {
"minRequests": 3
}
}
UI Highlighting
Target DOM elements to draw attention:
{
"uiHighlight": {
"selector": "#create-token-button", // CSS selector
"position": "right", // tooltip position
"offset": {
"x": 10,
"y": 0
}
}
}
Code Examples
Display copyable code snippets:
{
"codeExample": {
"language": "bash",
"code": "jetty share http://localhost:8000",
"copyable": true,
"explanation": "This creates a public tunnel"
}
}
Interactive Demos
Simulate terminal interactions:
{
"interactiveDemo": {
"type": "terminal-simulation",
"steps": [
{
"input": "jetty auth:login",
"output": "Enter your API token: "
},
{
"input": "jetty_xxxxxxxxxxxx",
"output": " Authentication successful!"
}
]
}
}
Frontend Implementation
Tutorial Engine Component
Create a React component to manage tutorial state:
// components/Tutorial/TutorialEngine.tsx
interface TutorialState {
currentStepIndex: number;
completedSteps: string[];
skippedSteps: string[];
validationInProgress: boolean;
tutorialData: Tutorial;
}
const TutorialEngine: React.FC<{ tutorialId: string }> = ({ tutorialId }) => {
const [state, setState] = useState<TutorialState>({
currentStepIndex: 0,
completedSteps: [],
skippedSteps: [],
validationInProgress: false,
tutorialData: null
});
useEffect(() => {
// Load tutorial JSON
fetch(`/api/tutorials/${tutorialId}`)
.then(res => res.json())
.then(data => setState(prev => ({ ...prev, tutorialData: data })));
}, [tutorialId]);
const currentStep = state.tutorialData?.steps[state.currentStepIndex];
return (
<div className="tutorial-container">
<TutorialHeader tutorial={state.tutorialData} />
<ProgressBar
current={state.currentStepIndex}
total={state.tutorialData?.steps.length}
/>
<StepRenderer
step={currentStep}
onComplete={handleStepComplete}
onSkip={handleStepSkip}
/>
</div>
);
};
Step Renderer
Render individual step content:
// components/Tutorial/StepRenderer.tsx
const StepRenderer: React.FC<{ step: Step; onComplete: () => void }> = ({
step,
onComplete
}) => {
const [validating, setValidating] = useState(false);
const handleValidation = async () => {
if (step.validation.type === 'manual') {
onComplete();
return;
}
setValidating(true);
// Poll validation endpoint
const pollValidation = setInterval(async () => {
const response = await fetch(step.validation.endpoint);
const result = await response.json();
if (result.valid) {
clearInterval(pollValidation);
setValidating(false);
showCelebration(step.celebration);
onComplete();
}
}, step.validation.pollInterval);
// Timeout
setTimeout(() => {
clearInterval(pollValidation);
setValidating(false);
}, step.validation.timeout);
};
return (
<div className="step-content">
<StepHeader icon={step.icon} title={step.title} />
<MarkdownContent content={step.content} />
{step.codeExample && (
<CodeBlock
language={step.codeExample.language}
code={step.codeExample.code}
copyable={step.codeExample.copyable}
/>
)}
{step.tips && <TipsPanel tips={step.tips} />}
{step.troubleshooting && <TroubleshootingPanel items={step.troubleshooting} />}
<button
onClick={handleValidation}
disabled={validating}
className="next-button"
>
{validating ? 'Validating...' : step.nextButton}
</button>
</div>
);
};
UI Highlighting System
Highlight specific UI elements during tutorial steps:
// components/Tutorial/UIHighlight.tsx
const UIHighlight: React.FC<{ highlight: UIHighlightConfig }> = ({ highlight }) => {
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
useEffect(() => {
const element = document.querySelector(highlight.selector);
if (element) {
const rect = element.getBoundingClientRect();
setTargetRect(rect);
// Add spotlight overlay
element.classList.add('tutorial-highlight');
return () => {
element.classList.remove('tutorial-highlight');
};
}
}, [highlight.selector]);
if (!targetRect) return null;
// Position tooltip based on highlight.position
const tooltipStyle = calculateTooltipPosition(targetRect, highlight.position, highlight.offset);
return (
<>
{/* Dimmed overlay */}
<div className="tutorial-overlay" />
{/* Spotlight cutout */}
<div
className="tutorial-spotlight"
style={{
left: targetRect.left,
top: targetRect.top,
width: targetRect.width,
height: targetRect.height
}}
/>
{/* Tooltip */}
<div className="tutorial-tooltip" style={tooltipStyle}>
{/* Tooltip content */}
</div>
</>
);
};
Celebration Animations
Show visual feedback on step completion:
// components/Tutorial/Celebrations.tsx
const showCelebration = (celebration: CelebrationConfig) => {
switch (celebration.animation) {
case 'confetti-small':
confetti({ particleCount: 50, spread: 60 });
break;
case 'confetti-large':
confetti({ particleCount: 200, spread: 90 });
break;
case 'check-bounce':
animateCheckmark();
break;
case 'rocket':
animateRocket();
break;
}
toast.success(celebration.message, {
duration: 3000,
icon: 'check'
});
};
Backend Implementation
Validation API Endpoints
Create Laravel routes for validation:
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
Route::get('/tutorial/validate/token-exists', [TutorialValidationController::class, 'validateTokenExists']);
Route::get('/tutorial/validate/cli-authenticated', [TutorialValidationController::class, 'validateCliAuthenticated']);
Route::get('/tutorial/validate/tunnel-active', [TutorialValidationController::class, 'validateTunnelActive']);
Route::get('/tutorial/validate/tunnel-request', [TutorialValidationController::class, 'validateTunnelRequest']);
Route::get('/tutorial/validate/multiple-requests', [TutorialValidationController::class, 'validateMultipleRequests']);
Route::get('/tutorial/validate/dashboard-viewed', [TutorialValidationController::class, 'validateDashboardViewed']);
Route::get('/tutorial/validate/tunnel-stopped', [TutorialValidationController::class, 'validateTunnelStopped']);
});
Validation Controller
Implement validation logic:
// app/Http/Controllers/TutorialValidationController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TutorialValidationController extends Controller
{
public function validateTokenExists(Request $request): JsonResponse
{
$user = $request->user();
// Check if user has created at least one API token
$hasToken = $user->tokens()->count() > 0;
return response()->json([
'valid' => $hasToken,
'message' => $hasToken ? 'Token found' : 'No token created yet'
]);
}
public function validateCliAuthenticated(Request $request): JsonResponse
{
$user = $request->user();
// Check for recent CLI authentication (token used within last 5 minutes)
$hasRecentCliAuth = $user->tokens()
->where('last_used_at', '>', now()->subMinutes(5))
->exists();
return response()->json([
'valid' => $hasRecentCliAuth,
'message' => $hasRecentCliAuth ? 'CLI authenticated' : 'Waiting for CLI authentication'
]);
}
public function validateTunnelActive(Request $request): JsonResponse
{
$user = $request->user();
// Check for active tunnel
$activeTunnel = $user->tunnels()
->where('status', 'active')
->latest()
->first();
return response()->json([
'valid' => $activeTunnel !== null,
'message' => $activeTunnel ? 'Tunnel is active' : 'No active tunnel',
'data' => $activeTunnel ? [
'id' => $activeTunnel->id,
'subdomain' => $activeTunnel->subdomain,
'url' => $activeTunnel->public_url
] : null
]);
}
public function validateTunnelRequest(Request $request): JsonResponse
{
$user = $request->user();
// Check if any tunnel has received at least one request
$hasRequest = $user->tunnels()
->whereHas('requests')
->exists();
return response()->json([
'valid' => $hasRequest,
'message' => $hasRequest ? 'Request received' : 'No requests yet'
]);
}
public function validateMultipleRequests(Request $request): JsonResponse
{
$user = $request->user();
$minRequests = $request->query('minRequests', 3);
// Get most recent tunnel
$tunnel = $user->tunnels()->latest()->first();
if (!$tunnel) {
return response()->json([
'valid' => false,
'message' => 'No tunnel found'
]);
}
$requestCount = $tunnel->requests()->count();
return response()->json([
'valid' => $requestCount >= $minRequests,
'message' => "Received {$requestCount} of {$minRequests} requests",
'data' => [
'current' => $requestCount,
'required' => $minRequests
]
]);
}
public function validateDashboardViewed(Request $request): JsonResponse
{
// This could check session data or analytics events
// For now, we'll use a simple flag stored in the session
$viewed = session()->has('tutorial_dashboard_viewed');
return response()->json([
'valid' => $viewed,
'message' => $viewed ? 'Dashboard viewed' : 'Dashboard not viewed yet'
]);
}
public function validateTunnelStopped(Request $request): JsonResponse
{
$user = $request->user();
// Check that the most recent tunnel is now inactive
$latestTunnel = $user->tunnels()->latest()->first();
$isStopped = $latestTunnel && $latestTunnel->status !== 'active';
return response()->json([
'valid' => $isStopped,
'message' => $isStopped ? 'Tunnel stopped' : 'Tunnel still active'
]);
}
}
Tutorial Progress Tracking
Track user progress through tutorials:
// app/Models/TutorialProgress.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TutorialProgress extends Model
{
protected $fillable = [
'user_id',
'tutorial_id',
'current_step_id',
'completed_steps',
'skipped_steps',
'completed_at',
'started_at'
];
protected $casts = [
'completed_steps' => 'array',
'skipped_steps' => 'array',
'started_at' => 'datetime',
'completed_at' => 'datetime'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function isCompleted(): bool
{
return $this->completed_at !== null;
}
public function percentComplete(): float
{
$tutorial = $this->getTutorialDefinition();
$totalSteps = count($tutorial['steps']);
$completedCount = count($this->completed_steps);
return ($completedCount / $totalSteps) * 100;
}
private function getTutorialDefinition(): array
{
$path = resource_path("tutorials/{$this->tutorial_id}.json");
return json_decode(file_get_contents($path), true);
}
}
Integration Points
Dashboard Integration
Add tutorial trigger to dashboard:
// resources/views/dashboard.blade.php
@if(!$user->hasCompletedTutorial('your-first-tunnel'))
<div class="tutorial-prompt">
<h3>New to Jetty?</h3>
<p>Take a 5-minute tutorial to create your first tunnel</p>
<button onclick="startTutorial('your-first-tunnel')">
Start Tutorial
</button>
</div>
@endif
CLI Coordination
The CLI should set appropriate flags when users perform actions:
// jetty-client/src/Commands/ShareCommand.php
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... existing share logic ...
// Update last_used_at for CLI authentication validation
$this->updateTokenUsage();
// Mark tunnel as active
$this->apiClient->post("/tunnels/{$tunnelId}/activate");
// ... rest of implementation ...
}
Styling
CSS Structure
/* Tutorial overlay and spotlight */
.tutorial-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9998;
backdrop-filter: blur(2px);
}
.tutorial-spotlight {
position: fixed;
background: white;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
border-radius: 8px;
z-index: 9999;
pointer-events: none;
transition: all 0.3s ease;
}
.tutorial-highlight {
position: relative;
z-index: 10000;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
border-radius: 8px;
}
/* Tutorial tooltip */
.tutorial-tooltip {
position: fixed;
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-width: 400px;
z-index: 10001;
}
/* Step content */
.step-content {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
.code-block {
background: #1e1e1e;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
position: relative;
}
.copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
opacity: 0.7;
transition: opacity 0.2s;
}
.copy-button:hover {
opacity: 1;
}
/* Progress bar */
.progress-bar {
height: 4px;
background: #e5e7eb;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transition: width 0.3s ease;
}
Testing
Frontend Tests
// __tests__/TutorialEngine.test.tsx
describe('TutorialEngine', () => {
it('loads tutorial definition', async () => {
const { getByText } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
await waitFor(() => {
expect(getByText('Share Your First Tunnel')).toBeInTheDocument();
});
});
it('progresses through steps', async () => {
const { getByText, getByRole } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
// Complete first step
const nextButton = getByRole('button', { name: /I've created my token/i });
fireEvent.click(nextButton);
await waitFor(() => {
expect(getByText('Open Your Terminal')).toBeInTheDocument();
});
});
it('validates steps correctly', async () => {
// Mock validation endpoint
fetchMock.get('/api/tutorial/validate/token-exists', {
valid: true,
message: 'Token found'
});
const { getByRole, getByText } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
const nextButton = getByRole('button', { name: /I've created my token/i });
fireEvent.click(nextButton);
await waitFor(() => {
expect(getByText(/Token created successfully/i)).toBeInTheDocument();
});
});
});
Backend Tests
// tests/Feature/TutorialValidationTest.php
class TutorialValidationTest extends TestCase
{
public function test_validates_token_exists()
{
$user = User::factory()->create();
$user->createToken('test-token');
$response = $this->actingAs($user)
->getJson('/api/tutorial/validate/token-exists');
$response->assertJson([
'valid' => true
]);
}
public function test_validates_active_tunnel()
{
$user = User::factory()->create();
$tunnel = Tunnel::factory()->create([
'user_id' => $user->id,
'status' => 'active'
]);
$response = $this->actingAs($user)
->getJson('/api/tutorial/validate/tunnel-active');
$response->assertJson([
'valid' => true,
'data' => [
'id' => $tunnel->id
]
]);
}
public function test_validates_multiple_requests()
{
$user = User::factory()->create();
$tunnel = Tunnel::factory()->create(['user_id' => $user->id]);
// Create 5 requests
TunnelRequest::factory()->count(5)->create([
'tunnel_id' => $tunnel->id
]);
$response = $this->actingAs($user)
->getJson('/api/tutorial/validate/multiple-requests?minRequests=3');
$response->assertJson([
'valid' => true,
'data' => [
'current' => 5,
'required' => 3
]
]);
}
}
Best Practices
Content Writing
- Be conversational - Write like you're guiding a friend
- Use clear CTAs - Every step should have one obvious next action
- Provide context - Explain why each step matters
- Anticipate confusion - Add troubleshooting for common issues
- Celebrate progress - Positive reinforcement keeps users engaged
Validation Design
- Poll frequently - 2-3 second intervals for responsive feedback
- Set reasonable timeouts - 5 minutes is usually sufficient
- Provide partial progress - Show "2 of 3 requests received" style feedback
- Handle edge cases - What if user has old tunnels/tokens?
- Graceful degradation - Allow manual override if validation fails
UI/UX Guidelines
- Don't block the UI - Users should be able to close tutorial anytime
- Persist progress - Save state so users can resume later
- Mobile-friendly - Tutorial should work on all screen sizes
- Accessible - Proper ARIA labels, keyboard navigation
- Performance - Don't poll validation endpoints too aggressively
Extensibility
Adding New Tutorials
- Create JSON definition in
docs/tutorials/{tutorial-id}.json - Implement any custom validation endpoints needed
- Add tutorial to the catalog in
docs/tutorials/catalog.json - Create accompanying markdown documentation
- Add tests for validation logic
Custom Validation Types
Register custom validators:
// lib/tutorialValidators.ts
const customValidators = {
'webhook_configured': async (step: Step) => {
const response = await fetch('/api/validate/webhook');
return response.json();
},
'custom_domain_verified': async (step: Step) => {
const response = await fetch('/api/validate/domain');
return response.json();
}
};
export const validateStep = async (step: Step) => {
const validator = customValidators[step.validation.type];
if (validator) {
return await validator(step);
}
// Fall back to API endpoint
return fetch(step.validation.endpoint).then(r => r.json());
};
Performance Considerations
Caching
Cache tutorial definitions in Redis:
// app/Services/TutorialService.php
public function getTutorial(string $tutorialId): array
{
return Cache::remember("tutorial:{$tutorialId}", 3600, function () use ($tutorialId) {
$path = resource_path("tutorials/{$tutorialId}.json");
return json_decode(file_get_contents($path), true);
});
}
Validation Optimization
Batch validation checks where possible:
public function validateMultipleSteps(Request $request): JsonResponse
{
$checks = $request->input('checks', []);
$results = [];
foreach ($checks as $check) {
$results[$check] = $this->runValidation($check);
}
return response()->json($results);
}
Future Enhancements
Potential improvements to the tutorial system:
- A/B testing - Test different tutorial flows to optimize completion rates
- Analytics - Track where users drop off to improve problematic steps
- Branching paths - Different flows based on user's framework (Laravel vs Node.js)
- Video integration - Embed short screencasts for complex steps
- Interactive CLI simulation - Browser-based terminal emulator for practice
- Progress sync - Sync tutorial progress across devices
- Achievements system - Unlock badges and rewards for completing tutorials
- Social sharing - Share completion with team or on social media
Related Documentation
- CLI Reference - Complete CLI command documentation
- API Reference - API endpoints and authentication
- Tunnels Reference - Tunnel features and configuration
Support
For questions or issues implementing tutorials:
- Open an issue on GitHub
- Check existing tutorials for patterns
- Review test cases for validation examples
- Consult the UI component library for standard elements
Send feedback
Found an issue or have a suggestion? Let us know.