Documentation

Full-Stack Example

Build a complete task management application with authentication, real-time updates, and file uploads.

Project Overview

This example builds a sophisticated task management app featuring:

  • User authentication with sessions
  • Real-time updates using Server-Sent Events
  • File attachments with local storage
  • Advanced filtering and search
  • Data export functionality

Initial Setup

# Create project
npm create svelte@latest task-manager
cd task-manager
npm install

# Install dependencies
npm install -D sveltekit-exec-adapter
npm install better-sqlite3 bcrypt multer uuid
npm install -D @types/bcrypt @types/multer @types/uuid

Authentication System

1. Database Schema

// src/lib/database.js
import Database from 'better-sqlite3';
import { join } from 'path';
import { homedir } from 'os';
import { mkdirSync, existsSync } from 'fs';

const appDir = join(homedir(), '.task-manager');
if (!existsSync(appDir)) {
	mkdirSync(appDir, { recursive: true });
}

const dbPath = join(appDir, 'tasks.db');
export const db = new Database(dbPath);

// Create tables
db.exec(`
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		username TEXT UNIQUE NOT NULL,
		email TEXT UNIQUE NOT NULL,
		password_hash TEXT NOT NULL,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS sessions (
		id TEXT PRIMARY KEY,
		user_id INTEGER NOT NULL,
		expires_at DATETIME NOT NULL,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
	);

	CREATE TABLE IF NOT EXISTS tasks (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		user_id INTEGER NOT NULL,
		title TEXT NOT NULL,
		description TEXT,
		status TEXT DEFAULT 'pending',
		priority TEXT DEFAULT 'medium',
		due_date DATETIME,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
	);

	CREATE TABLE IF NOT EXISTS task_attachments (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		task_id INTEGER NOT NULL,
		filename TEXT NOT NULL,
		original_name TEXT NOT NULL,
		mime_type TEXT NOT NULL,
		file_size INTEGER NOT NULL,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE
	);

	CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
	CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
	CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
`);

// Prepared statements
export const queries = {
	// Users
	createUser: db.prepare('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)'),
	getUserByEmail: db.prepare('SELECT * FROM users WHERE email = ?'),
	getUserById: db.prepare('SELECT * FROM users WHERE id = ?'),

	// Sessions
	createSession: db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)'),
	getSession: db.prepare('SELECT * FROM sessions WHERE id = ? AND expires_at > datetime("now")'),
	deleteSession: db.prepare('DELETE FROM sessions WHERE id = ?'),
	cleanupSessions: db.prepare('DELETE FROM sessions WHERE expires_at <= datetime("now")'),

	// Tasks
	createTask: db.prepare(
		'INSERT INTO tasks (user_id, title, description, status, priority, due_date) VALUES (?, ?, ?, ?, ?, ?)'
	),
	getUserTasks: db.prepare('SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at DESC'),
	getTaskById: db.prepare('SELECT * FROM tasks WHERE id = ? AND user_id = ?'),
	updateTask: db.prepare(
		'UPDATE tasks SET title = ?, description = ?, status = ?, priority = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?'
	),
	deleteTask: db.prepare('DELETE FROM tasks WHERE id = ? AND user_id = ?'),
	searchTasks: db.prepare(
		'SELECT * FROM tasks WHERE user_id = ? AND (title LIKE ? OR description LIKE ?) ORDER BY created_at DESC'
	),

	// Attachments
	createAttachment: db.prepare(
		'INSERT INTO task_attachments (task_id, filename, original_name, mime_type, file_size) VALUES (?, ?, ?, ?, ?)'
	),
	getTaskAttachments: db.prepare('SELECT * FROM task_attachments WHERE task_id = ?'),
	deleteAttachment: db.prepare(
		'DELETE FROM task_attachments WHERE id = ? AND task_id IN (SELECT id FROM tasks WHERE user_id = ?)'
	)
};

// Cleanup old sessions on startup
queries.cleanupSessions.run();

2. Authentication Utilities

// src/lib/auth.js
import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
import { queries } from './database.js';

export class AuthService {
	static async createUser(username, email, password) {
		const passwordHash = await bcrypt.hash(password, 12);

		try {
			const result = queries.createUser.run(username, email, passwordHash);
			return { id: result.lastInsertRowid, username, email };
		} catch (error) {
			if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
				throw new Error('Email or username already exists');
			}
			throw error;
		}
	}

	static async validateUser(email, password) {
		const user = queries.getUserByEmail.get(email);
		if (!user) return null;

		const isValid = await bcrypt.compare(password, user.password_hash);
		if (!isValid) return null;

		return { id: user.id, username: user.username, email: user.email };
	}

	static createSession(userId) {
		const sessionId = randomUUID();
		const expiresAt = new Date();
		expiresAt.setDate(expiresAt.getDate() + 7); // 7 days

		queries.createSession.run(sessionId, userId, expiresAt.toISOString());
		return sessionId;
	}

	static getSessionUser(sessionId) {
		if (!sessionId) return null;

		const session = queries.getSession.get(sessionId);
		if (!session) return null;

		const user = queries.getUserById.get(session.user_id);
		return user ? { id: user.id, username: user.username, email: user.email } : null;
	}

	static deleteSession(sessionId) {
		queries.deleteSession.run(sessionId);
	}
}

3. Authentication Hooks

// src/hooks.server.js
import { AuthService } from '$lib/auth.js';

export async function handle({ event, resolve }) {
	// Get session from cookie
	const sessionId = event.cookies.get('session');
	event.locals.user = AuthService.getSessionUser(sessionId);

	return resolve(event);
}

API Routes

4. Authentication Endpoints

// src/routes/api/auth/register/+server.js
import { json } from '@sveltejs/kit';
import { AuthService } from '$lib/auth.js';

export async function POST({ request, cookies }) {
	try {
		const { username, email, password } = await request.json();

		// Validation
		if (!username || username.length < 3) {
			return json({ error: 'Username must be at least 3 characters' }, { status: 400 });
		}

		if (!email || !email.includes('@')) {
			return json({ error: 'Valid email is required' }, { status: 400 });
		}

		if (!password || password.length < 6) {
			return json({ error: 'Password must be at least 6 characters' }, { status: 400 });
		}

		// Create user
		const user = await AuthService.createUser(username, email, password);
		const sessionId = AuthService.createSession(user.id);

		// Set session cookie
		cookies.set('session', sessionId, {
			path: '/',
			httpOnly: true,
			secure: false, // Set to true in production with HTTPS
			sameSite: 'strict',
			maxAge: 60 * 60 * 24 * 7 // 7 days
		});

		return json({ user });
	} catch (error) {
		return json({ error: error.message }, { status: 400 });
	}
}
// src/routes/api/auth/login/+server.js
import { json } from '@sveltejs/kit';
import { AuthService } from '$lib/auth.js';

export async function POST({ request, cookies }) {
	try {
		const { email, password } = await request.json();

		const user = await AuthService.validateUser(email, password);
		if (!user) {
			return json({ error: 'Invalid credentials' }, { status: 401 });
		}

		const sessionId = AuthService.createSession(user.id);

		cookies.set('session', sessionId, {
			path: '/',
			httpOnly: true,
			secure: false,
			sameSite: 'strict',
			maxAge: 60 * 60 * 24 * 7
		});

		return json({ user });
	} catch (error) {
		return json({ error: error.message }, { status: 500 });
	}
}
// src/routes/api/auth/logout/+server.js
import { json } from '@sveltejs/kit';
import { AuthService } from '$lib/auth.js';

export async function POST({ cookies }) {
	const sessionId = cookies.get('session');

	if (sessionId) {
		AuthService.deleteSession(sessionId);
	}

	cookies.delete('session', { path: '/' });
	return json({ success: true });
}

5. Task Management API

// src/routes/api/tasks/+server.js
import { json } from '@sveltejs/kit';
import { queries } from '$lib/database.js';

export async function GET({ locals, url }) {
	if (!locals.user) {
		return json({ error: 'Unauthorized' }, { status: 401 });
	}

	const search = url.searchParams.get('search');
	let tasks;

	if (search) {
		const searchTerm = `%${search}%`;
		tasks = queries.searchTasks.all(locals.user.id, searchTerm, searchTerm);
	} else {
		tasks = queries.getUserTasks.all(locals.user.id);
	}

	return json(tasks);
}

export async function POST({ request, locals }) {
	if (!locals.user) {
		return json({ error: 'Unauthorized' }, { status: 401 });
	}

	try {
		const {
			title,
			description,
			status = 'pending',
			priority = 'medium',
			due_date
		} = await request.json();

		if (!title) {
			return json({ error: 'Title is required' }, { status: 400 });
		}

		const result = queries.createTask.run(
			locals.user.id,
			title,
			description || null,
			status,
			priority,
			due_date || null
		);

		const task = queries.getTaskById.get(result.lastInsertRowid, locals.user.id);
		return json(task, { status: 201 });
	} catch (error) {
		return json({ error: 'Failed to create task' }, { status: 500 });
	}
}

6. File Upload System

// src/routes/api/tasks/[id]/attachments/+server.js
import { json } from '@sveltejs/kit';
import { queries } from '$lib/database.js';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';
import { randomUUID } from 'crypto';

const uploadsDir = join(homedir(), '.task-manager', 'uploads');

export async function POST({ request, params, locals }) {
	if (!locals.user) {
		return json({ error: 'Unauthorized' }, { status: 401 });
	}

	// Verify task ownership
	const task = queries.getTaskById.get(params.id, locals.user.id);
	if (!task) {
		return json({ error: 'Task not found' }, { status: 404 });
	}

	try {
		const formData = await request.formData();
		const file = formData.get('file');

		if (!file || !(file instanceof File)) {
			return json({ error: 'No file provided' }, { status: 400 });
		}

		// Validate file
		const maxSize = 10 * 1024 * 1024; // 10MB
		if (file.size > maxSize) {
			return json({ error: 'File too large (max 10MB)' }, { status: 400 });
		}

		// Generate unique filename
		const fileExtension = file.name.split('.').pop();
		const filename = `${randomUUID()}.${fileExtension}`;
		const filepath = join(uploadsDir, filename);

		// Ensure uploads directory exists
		await mkdir(uploadsDir, { recursive: true });

		// Save file
		const buffer = Buffer.from(await file.arrayBuffer());
		await writeFile(filepath, buffer);

		// Save to database
		const result = queries.createAttachment.run(task.id, filename, file.name, file.type, file.size);

		return json(
			{
				id: result.lastInsertRowid,
				filename,
				original_name: file.name,
				mime_type: file.type,
				file_size: file.size
			},
			{ status: 201 }
		);
	} catch (error) {
		console.error('Upload error:', error);
		return json({ error: 'Upload failed' }, { status: 500 });
	}
}

export async function GET({ params, locals }) {
	if (!locals.user) {
		return json({ error: 'Unauthorized' }, { status: 401 });
	}

	// Verify task ownership
	const task = queries.getTaskById.get(params.id, locals.user.id);
	if (!task) {
		return json({ error: 'Task not found' }, { status: 404 });
	}

	const attachments = queries.getTaskAttachments.all(task.id);
	return json(attachments);
}

Real-Time Updates

7. Server-Sent Events

// src/routes/api/events/+server.js
export async function GET({ locals }) {
	if (!locals.user) {
		return new Response('Unauthorized', { status: 401 });
	}

	return new Response(
		new ReadableStream({
			start(controller) {
				// Send initial connection message
				controller.enqueue(
					`data: ${JSON.stringify({ type: 'connected', userId: locals.user.id })}\n\n`
				);

				// Keep connection alive
				const keepAlive = setInterval(() => {
					controller.enqueue(
						`data: ${JSON.stringify({ type: 'ping', timestamp: Date.now() })}\n\n`
					);
				}, 30000);

				// Store reference for cleanup
				controller.keepAlive = keepAlive;
			},
			cancel() {
				if (controller.keepAlive) {
					clearInterval(controller.keepAlive);
				}
			}
		}),
		{
			headers: {
				'Content-Type': 'text/event-stream',
				'Cache-Control': 'no-cache',
				Connection: 'keep-alive'
			}
		}
	);
}

8. Task Store with Real-Time Updates

// src/lib/stores/tasks.js
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';

export const tasks = writable([]);
export const searchTerm = writable('');
export const filter = writable('all');

// Filtered tasks
export const filteredTasks = derived(
	[tasks, searchTerm, filter],
	([$tasks, $searchTerm, $filter]) => {
		let filtered = $tasks;

		// Apply search filter
		if ($searchTerm) {
			const term = $searchTerm.toLowerCase();
			filtered = filtered.filter(
				(task) =>
					task.title.toLowerCase().includes(term) ||
					(task.description && task.description.toLowerCase().includes(term))
			);
		}

		// Apply status filter
		if ($filter !== 'all') {
			filtered = filtered.filter((task) => task.status === $filter);
		}

		return filtered;
	}
);

export class TaskService {
	static eventSource = null;

	static async loadTasks() {
		try {
			const response = await fetch('/api/tasks');
			if (response.ok) {
				const data = await response.json();
				tasks.set(data);
			}
		} catch (error) {
			console.error('Failed to load tasks:', error);
		}
	}

	static async createTask(taskData) {
		try {
			const response = await fetch('/api/tasks', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(taskData)
			});

			if (response.ok) {
				await this.loadTasks(); // Refresh
				return await response.json();
			}
		} catch (error) {
			console.error('Failed to create task:', error);
		}
	}

	static async updateTask(id, taskData) {
		try {
			const response = await fetch(`/api/tasks/${id}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(taskData)
			});

			if (response.ok) {
				await this.loadTasks(); // Refresh
				return await response.json();
			}
		} catch (error) {
			console.error('Failed to update task:', error);
		}
	}

	static async deleteTask(id) {
		try {
			const response = await fetch(`/api/tasks/${id}`, {
				method: 'DELETE'
			});

			if (response.ok) {
				await this.loadTasks(); // Refresh
			}
		} catch (error) {
			console.error('Failed to delete task:', error);
		}
	}

	static connectRealTime() {
		if (!browser || this.eventSource) return;

		this.eventSource = new EventSource('/api/events');

		this.eventSource.onmessage = (event) => {
			const data = JSON.parse(event.data);

			switch (data.type) {
				case 'task_updated':
				case 'task_created':
				case 'task_deleted':
					this.loadTasks(); // Refresh on any task change
					break;
			}
		};

		this.eventSource.onerror = () => {
			console.warn('EventSource connection lost, reconnecting...');
			setTimeout(() => {
				this.connectRealTime();
			}, 5000);
		};
	}

	static disconnect() {
		if (this.eventSource) {
			this.eventSource.close();
			this.eventSource = null;
		}
	}
}

Frontend Components

9. Authentication Forms

<!-- src/lib/components/LoginForm.svelte -->
<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	let email = '';
	let password = '';
	let loading = false;
	let error = '';

	async function handleSubmit() {
		if (!email || !password) {
			error = 'Please fill in all fields';
			return;
		}

		loading = true;
		error = '';

		try {
			const response = await fetch('/api/auth/login', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ email, password })
			});

			const data = await response.json();

			if (response.ok) {
				dispatch('success', data.user);
			} else {
				error = data.error || 'Login failed';
			}
		} catch (err) {
			error = 'Network error. Please try again.';
		} finally {
			loading = false;
		}
	}
</script>

<form on:submit|preventDefault={handleSubmit} class="auth-form">
	<h2>Sign In</h2>

	{#if error}
		<div class="error">{error}</div>
	{/if}

	<div class="form-group">
		<label for="email">Email</label>
		<input
			id="email"
			type="email"
			bind:value={email}
			placeholder="Enter your email"
			disabled={loading}
			required
		/>
	</div>

	<div class="form-group">
		<label for="password">Password</label>
		<input
			id="password"
			type="password"
			bind:value={password}
			placeholder="Enter your password"
			disabled={loading}
			required
		/>
	</div>

	<button type="submit" disabled={loading} class="submit-btn">
		{loading ? 'Signing in...' : 'Sign In'}
	</button>

	<p class="switch-form">
		Don't have an account?
		<button type="button" on:click={() => dispatch('switch')}>Sign up</button>
	</p>
</form>

<style>
	.auth-form {
		max-width: 400px;
		margin: 2rem auto;
		padding: 2rem;
		background: white;
		border-radius: 8px;
		box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
	}

	.auth-form h2 {
		text-align: center;
		margin-bottom: 1.5rem;
		color: #2d3748;
	}

	.error {
		background: #fed7d7;
		color: #c53030;
		padding: 0.75rem;
		border-radius: 4px;
		margin-bottom: 1rem;
		text-align: center;
	}

	.form-group {
		margin-bottom: 1rem;
	}

	.form-group label {
		display: block;
		margin-bottom: 0.5rem;
		font-weight: 500;
		color: #4a5568;
	}

	.form-group input {
		width: 100%;
		padding: 0.75rem;
		border: 1px solid #e2e8f0;
		border-radius: 4px;
		font-size: 1rem;
	}

	.form-group input:focus {
		outline: none;
		border-color: #3182ce;
		box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
	}

	.submit-btn {
		width: 100%;
		padding: 0.75rem;
		background: #3182ce;
		color: white;
		border: none;
		border-radius: 4px;
		font-size: 1rem;
		font-weight: 500;
		cursor: pointer;
		transition: background-color 0.2s;
	}

	.submit-btn:hover:not(:disabled) {
		background: #2c5aa0;
	}

	.submit-btn:disabled {
		opacity: 0.6;
		cursor: not-allowed;
	}

	.switch-form {
		text-align: center;
		margin-top: 1rem;
		color: #718096;
	}

	.switch-form button {
		background: none;
		border: none;
		color: #3182ce;
		cursor: pointer;
		text-decoration: underline;
	}
</style>

10. Main Application Layout

<!-- src/routes/+layout.svelte -->
<script>
	import { onMount } from 'svelte';
	import { goto } from '$app/navigation';
	import { page } from '$app/stores';
	import { userStore } from '$lib/stores/user.js';
	import { TaskService } from '$lib/stores/tasks.js';

	let loading = true;

	onMount(async () => {
		// Check if user is logged in
		try {
			const response = await fetch('/api/auth/me');
			if (response.ok) {
				const data = await response.json();
				userStore.set(data.user);
				TaskService.connectRealTime();
			}
		} catch (error) {
			console.error('Auth check failed:', error);
		} finally {
			loading = false;
		}

		// Cleanup on page unload
		return () => {
			TaskService.disconnect();
		};
	});

	$: isAuthPage = $page.route.id?.startsWith('/auth');
	$: if ($userStore && isAuthPage) {
		goto('/');
	}
	$: if (!$userStore && !isAuthPage && !loading) {
		goto('/auth');
	}
</script>

<svelte:head>
	<title>Task Manager</title>
	<meta name="description" content="A full-featured task management application" />
</svelte:head>

{#if loading}
	<div class="loading">
		<div class="spinner"></div>
		<p>Loading...</p>
	</div>
{:else}
	<slot />
{/if}

<style>
	:global(body) {
		margin: 0;
		font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
		background: #f7fafc;
		color: #2d3748;
	}

	.loading {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		height: 100vh;
		gap: 1rem;
	}

	.spinner {
		width: 32px;
		height: 32px;
		border: 3px solid #e2e8f0;
		border-top: 3px solid #3182ce;
		border-radius: 50%;
		animation: spin 1s linear infinite;
	}

	@keyframes spin {
		0% {
			transform: rotate(0deg);
		}
		100% {
			transform: rotate(360deg);
		}
	}
</style>

Build Configuration

11. Adapter Configuration

// svelte.config.js
import adapter from 'sveltekit-exec-adapter';

const config = {
	kit: {
		adapter: adapter({
			embedStatic: true,
			validation: {
				maxAssetSize: 50 * 1024 * 1024, // 50MB for file uploads
				allowedExtensions: [
					'.css',
					'.js',
					'.png',
					'.svg',
					'.ico',
					'.jpg',
					'.jpeg',
					'.pdf',
					'.doc',
					'.docx'
				]
			}
		}),
		prerender: { entries: [] }
	}
};

export default config;

12. Build Script

{
	"name": "task-manager",
	"scripts": {
		"dev": "vite dev",
		"build": "vite build",
		"start": "./build/task-manager",
		"clean": "rm -rf build .svelte-kit"
	},
	"dependencies": {
		"better-sqlite3": "^9.0.0",
		"bcrypt": "^5.1.0",
		"uuid": "^9.0.0"
	}
}

Features Demonstrated

This full-stack example showcases:

  1. Complete Authentication System: Registration, login, sessions
  2. Database Design: Normalized schema with relationships
  3. File Upload Handling: Secure file storage and retrieval
  4. Real-Time Updates: Server-Sent Events for live updates
  5. Advanced State Management: Svelte stores with derived state
  6. Security Best Practices: Input validation, SQL injection prevention
  7. Error Handling: Comprehensive error handling throughout
  8. Responsive Design: Mobile-friendly UI components

The resulting executable is a complete, production-ready task management application that runs anywhere without external dependencies!