A SvelteKit 5 + Tailwind frontend with i18n (Paraglide) and dark mode enabled by default.
@tailwindcss/vite, no separate config)@sveltejs/adapter-node (Node.js deployments)# Start dev server (port 3000, with hot reload)
npm run dev
# or with browser auto-open:
npm run dev -- --open
# Production build (generates .svelte-kit, /build, and optimized JS/CSS)
npm run build
# Preview production build locally (runs on port 3000 by default)
npm run preview
# Sync SvelteKit config (auto-runs on `npm install`)
npm run prepare
# Start production server (requires `npm run build` first)
npm start
This is a fresh project with no test suite or linter configured. Do not assume these exist.
Before first dev/build run, MUST copy configuration:
cp config.json.example config.json
Database is auto-initialized on first run (creates /data/app.db, seeds admin user admin@delete.me / deleteme).
src/
├── app.html # HTML shell (dark mode preset)
├── hooks.js # Paraglide i18n route localization
├── hooks.server.js # Graceful shutdown, db cleanup, signal handlers
├── routes/
│ ├── +layout.svelte # Root layout component
│ ├── +page.svelte # Home page (Flowbite Alert, language switcher, dark mode toggle)
│ ├── +error.svelte # Error page
│ └── layout.css # Route-level styles
└── lib/
├── index.js # $lib alias exports
├── config.js # Loads config.json (db & password settings)
├── crypto.js # Argon2id hashing & verification (hashPassword, verifyPassword)
├── sqlite3/
│ └── database.js # Database initialization, schema, admin user seeding
├── paraglide/ # i18n (auto-generated, DO NOT EDIT)
│ ├── messages.js # Locale-aware message functions
│ ├── runtime.js # getLocale(), setLocale(), deLocalizeUrl()
│ └── messages/*.js # en-us, es-mx translations
└── assets/ # Static assets
config.json (loaded by src/lib/config.js, must exist before runtime)/data/app.db on first run)users table with id, email, password (hashed), created_at, updated_atsrc/lib/crypto.jsadmin@delete.me / deleteme).svelte-kit/ — SvelteKit type hints & config cache/build/ — Production output (Node.js adapter)vite.config.js.timestamp-* — Vite cachevite.config.js)npm run preview)All code must use Svelte 5 runes by default. The Vite config enforces this for all files except those in node_modules:
runes: ({ filename }) =>
filename.split(/[/\\]/).includes('node_modules') ? undefined : true
Effect on Component Writing:
$state() instead of let for reactive variables$derived for computed values$effect() for side effects (replaces onMount, afterUpdate){#snippet name()} syntax (shown in +page.svelte)Action: Any new .svelte files must use runes. Violating this breaks dev/build.
@tailwindcss/vite plugin (not postcss)@tailwindcss/forms, @tailwindcss/typographyapp.html (class="dark" on <html>)tailwind.config.js (handled entirely via Vite plugin)@sveltejs/adapter-node for Node.js deployments/build/$env module for runtime env varsConvention: Clear, atomic commits. Prefix context: "feat:", "fix:", "docs:", "refactor:" when possible.
No CI/CD: No .github/workflows/ or other CI pipeline configured
No Tests: No test framework or test scripts in package.json
Minimal jsconfig: Inherits TypeScript config from .svelte-kit/tsconfig.json but doesn't enforce type checking (checkJs: false)
Config must exist: Application crashes at startup if config.json is missing. Always ensure it's copied from config.json.example before running dev/build.
pnpm-workspace.yaml native builds: Specifies which native packages can be built (esbuild, Tailwind Oxide, better-sqlite3 enabled; argon2 disabled). If native deps fail to build, check this file first.
flowbite-svelte{#snippet} blocks (Svelte 5 pattern)<style> blocks for scoped styles)dark: prefix (already enabled on root)+page.svelte (and optionally +page.js, +layout.svelte) in src/routes/[path]/+page.server.js (not configured; would need adapter setup).env file (gitignored)import { env } from '$env/dynamic/public' or $env/static/publicVITE_ for client accesssrc/lib/config.js)The application loads config.json at startup. This must exist before running dev/build:
cp config.json.example config.json
Failure mode: If missing, the app crashes with Failed to load config.json error during npm run dev or npm run build.
src/lib/sqlite3/database.js)getSqlite3() callusers table auto-created with id, email, password, created_at, updated_atadmin@delete.me / deleteme is inserted (hashed with Argon2id)data/app.db (relative to project root, constructed from config.json filename + PROJECT_ROOT/data/, directory auto-created if missing)Idempotent: Safe to call multiple times; schema and admin user only created once.
src/lib/crypto.js)Two exported functions:
hashPassword(password) — async, returns Argon2id hash (configured in config.json)verifyPassword(password, hash) — async, returns booleanUse these for any user auth operations. Config pulls settings from config.password.options.
src/hooks.server.js)Database connection is properly closed on process termination (SIGTERM, SIGINT) or runtime errors. Exit codes:
0 — Graceful shutdown (SIGTERM/SIGINT)1 — Uncaught exception or unhandled rejection2 — Cleanup error during shutdown/v1 API — OpenAI-Compatible LLM ProxyThe application exposes an OpenAI-compatible REST API under /v1 that proxies chat completion requests to configured LLM providers. All /v1 endpoints require OAuth 2.1 Bearer token authentication with the llm scope.
Every /v1 request must include an Authorization header:
Authorization: Bearer <access_token>
Tokens are issued by the local OAuth 2.1 authorization server. The flow is:
POST /oauth/registerPOST /oauth/login (sets session cookie)GET /oauth/authorize with PKCE S256POST /oauth/tokenSee scripts/get-token.sh for an automated script that runs the full flow.
Providers are defined in config.json under the providers array. Only providers with enabled: true are exposed.
Supported provider types:
| Type | Required Config | Credentials |
|---|---|---|
bedrock |
region, credentials |
AWS access key ID + secret |
anthropic |
apiKey |
Anthropic API key |
openai-compatible |
baseUrl |
None (local endpoint) |
Example config.json snippet:
{
"providers": [
{
"id": "bedrock",
"type": "bedrock",
"enabled": true,
"region": "us-east-1",
"credentials": { "accessKeyId": "...", "secretAccessKey": "..." },
"models": { "claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0" }
},
{
"id": "anthropic",
"type": "anthropic",
"enabled": true,
"apiKey": "...",
"models": { "claude-3-5-sonnet": "claude-3-5-sonnet-20241022" }
},
{
"id": "local",
"type": "openai-compatible",
"enabled": false,
"baseUrl": "http://localhost:11434/v1",
"models": { "llama3.1": "llama3.1:latest" }
}
]
}
Models are addressed as provider/localModelId (e.g., bedrock/claude-3-sonnet). The models object in config maps the local key to the provider's native identifier.
POST /v1/chat/completionsChat with streaming or non-streaming response.
# Non-streaming
curl -X POST http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"model": "bedrock/claude-3-sonnet",
"messages": [{"role": "user", "content": "Hello!"}],
"temperature": 0.7,
"max_tokens": 256
}'
# Streaming
curl -X POST http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-3-5-sonnet",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'
GET /v1/modelsList all available models across enabled providers.
curl http://localhost:3000/v1/models \
-H "Authorization: Bearer <token>"
GET /v1/models/{model}Get details for a single model. The slash in the model ID must be URL-encoded (%2F).
curl http://localhost:3000/v1/models/bedrock%2Fclaude-3-sonnet \
-H "Authorization: Bearer <token>"
All errors are returned in OpenAI-compatible JSON:
{
"error": {
"message": "Human-readable description",
"type": "invalid_request_error",
"param": null,
"code": "invalid_model"
}
}
Common error types: invalid_request_error, invalid_token, insufficient_scope, authentication_error, rate_limit_error, api_error.
Common HTTP status codes:
200 — Success400 — Invalid request (bad JSON, missing params, unknown model)401 — Missing or invalid Bearer token403 — Valid token but insufficient scope404 — Model not found429 — Rate limited by provider500 — Internal server error (provider init failure)502 — Provider service error503 — Provider overloadedsrc/routes/v1/chat/completions/+server.js — Chat completion handlersrc/routes/v1/models/+server.js — Model list handlersrc/routes/v1/models/[model]/+server.js — Model detail handlersrc/lib/providers/index.js — Provider resolution and model listingsrc/lib/providers/errors.js — Error normalization to OpenAI formatsrc/lib/providers/bedrock.js — AWS Bedrock providersrc/lib/providers/anthropic.js — Anthropic providersrc/lib/providers/openai-compatible.js — Generic OpenAI-compatible providersrc/lib/oauth/bearer.js — Bearer token validation--profile if neededWhen using docker run or docker-compose, the config is mounted at /opt/app/server/config.json. The database path data/app.db is then resolved relative to the working directory inside the container, which is typically /opt/app/. Ensure volume mounts for ./data are in place to persist the database across restarts.
The Dockerfile uses Node.js 24 Alpine. Native packages (better-sqlite3, argon2) must build in the Alpine environment. If builds fail, check:
pnpm-workspace.yaml allowlist is currentLast Updated: Verified against current project state (15 commits, Docker support added, SvelteKit 2.68.0, Vite 8.1.2, pnpm-workspace.yaml quirks documented)