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:
- Complete Authentication System: Registration, login, sessions
- Database Design: Normalized schema with relationships
- File Upload Handling: Secure file storage and retrieval
- Real-Time Updates: Server-Sent Events for live updates
- Advanced State Management: Svelte stores with derived state
- Security Best Practices: Input validation, SQL injection prevention
- Error Handling: Comprehensive error handling throughout
- Responsive Design: Mobile-friendly UI components
The resulting executable is a complete, production-ready task management application that runs anywhere without external dependencies!