SvelteKit + DaisyUI v5 + Fastify + MongoDB, containerized with Docker.
The candidate/ directory is the scaffold.
See candidate/CLAUDE.md for the full project reference.
| Layer | Package | Version |
|---|---|---|
| Frontend | SvelteKit + Svelte | 2 / 5 |
| Components | DaisyUI | v5 |
| CSS | Tailwind | v4 |
| API | Fastify | v5 |
| Database | MongoDB | 7 |
| Auth | @fastify/session + bcryptjs | — |
| Nodemailer (Ethereal dev / SMTP prod) | — | |
| Container | Docker Compose | — |
cd candidate
cp .env.example .env
# Edit .env — set SESSION_SECRET at minimum
cd frontend && npm install && cd ..
cd api && npm install && cd ..
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
Windows: Bind mounts don't propagate FS events reliably. API changes always require a rebuild; Svelte changes often do too. Never rely on hot-reload for server-side files.
docker compose -f docker-compose.yml -f docker-compose.dev.yml build api
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d api
docker compose up --build -d
docker compose down # stop, keep volumes
docker compose down -v # stop, delete volumes
GET /settings, PATCH /settings/:key; typed inputs (string/boolean/number/select)app.name setting updates the header and browser title in real time; admins can upload a custom logo image to replace the default SVG iconlogAudit() helper; events across auth/users/roles/messages/settings; GET /audit with paginationOLLAMA_URL=http://host.docker.internal:11434); can be disabled via chat.enabled settingcandidate/ # The scaffold — clone this for new projects
├── docker-compose.yml
├── docker-compose.dev.yml
├── .env.example
├── CLAUDE.md # Full project reference for AI-assisted dev
├── frontend/ # SvelteKit app — port 3000
│ └── src/
│ ├── routes/
│ │ ├── (marketing)/ # Public marketing pages
│ │ ├── api/ # SvelteKit → Fastify proxy routes
│ │ ├── dashboard/
│ │ ├── messages/
│ │ ├── settings/ # Admin: app configuration
│ │ ├── users/ # Admin: Manage Users
│ │ ├── roles/ # Admin: Manage Roles
│ │ ├── login/
│ │ ├── forgot-password/
│ │ └── reset-password/
│ └── lib/
│ ├── components/ # Shared UI components
│ ├── config/
│ │ ├── logo.ts # Brand name / logo
│ │ └── nav.ts # Sidebar nav items (module-extensible)
│ └── permissions.ts # hasPermission(user, resource, action)
└── api/ # Fastify app — port 4000
└── src/
├── server.ts # Entry point; routes auto-loaded from routes/
├── plugins/ # session, MongoDB, CORS, seed
├── routes/ # One subdirectory per resource (autoloaded)
├── data/
│ └── permissions.json # Default role permissions (module-extensible)
└── lib/ # checkDuplicateUser, email, audit helpers
modules/ # Build-time feature modules
├── commerce/ # E-commerce: products, categories, orders, inventory
│ ├── module.json
│ ├── api/src/routes/commerce/
│ └── frontend/src/routes/commerce/
├── calendar-events/ # Typed timed events with subscribe/notification model
│ ├── module.json
│ ├── api/src/routes/calendar-events/
│ ├── api/src/lib/calendarNotify.ts
│ └── frontend/src/routes/calendar-events/
└── notifications/ # Example stub module
├── module.json
├── api/src/routes/notifications/
└── frontend/src/routes/notifications/
arch.js # Project scaffold CLI (see below)
Established UI patterns for the scaffold. Follow these exactly when adding new pages or components.
| Intent | Classes |
|---|---|
| Primary action | btn btn-primary |
| Destructive | btn btn-error btn-outline |
| Cancel / secondary | btn btn-ghost |
| Compact (in tables, toolbars) | add btn-sm |
| Icon-only | btn btn-ghost btn-square btn-sm |
Every route page that has a title uses this structure — do not inline it differently:
<div>
<h1 class="text-2xl font-bold">{title}</h1>
<p class="text-sm opacity-60">{subtitle}</p>
</div>
<div class="card bg-base-100 border border-base-200">
<div class="p-6 space-y-4">
<!-- content -->
</div>
</div>
Padding goes on the inner div, not on .card itself.
Every <tbody> row must use the alternating-row class string:
<tr class="odd:bg-transparent even:bg-black/[.025] dark:even:bg-white/[.035] hover:bg-black/[.05] dark:hover:bg-white/[.06] transition-colors">
Custom fixed overlay — do not use DaisyUI .modal:
{#if open}
<div transition:fade class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
<div transition:scale class="card bg-base-100 border border-base-200 w-full max-w-md shadow-xl">
<header class="flex items-center justify-between px-6 pt-5 pb-3 border-b border-base-200">
<h2 class="font-semibold">{title}</h2>
<button class="btn btn-ghost btn-square btn-sm" onclick={() => open = false}>
<X class="size-4" />
</button>
</header>
<div class="p-6 space-y-4"><!-- body --></div>
<footer class="flex items-center justify-between px-6 pb-5 pt-3">
<div><!-- destructive action (delete) if applicable --></div>
<div class="flex gap-3">
<button class="btn btn-ghost" onclick={() => open = false}>Cancel</button>
<button class="btn btn-primary">Save</button>
</div>
</footer>
</div>
</div>
{/if}
| Use | Classes |
|---|---|
| Status label | badge badge-{color} |
| Status outline | badge badge-{color} badge-outline |
| Compact unread dot | min-w-[14px] h-[14px] px-[2px] rounded-full text-[10px] leading-[14px] text-center text-white bg-error |
<label class="input input-bordered flex items-center gap-2">
<Search class="size-4 opacity-50" />
<input class="grow" placeholder="Search…" bind:value={query} />
</label>
<div class="flex gap-1 border-b border-base-200">
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors
{active === 'tab' ? 'border-primary text-primary' : 'border-transparent hover:text-base-content'}"
onclick={() => active = 'tab'}
>Tab Label</button>
</div>
Use text-sm opacity-60 for subtitles, hints, and secondary info. Do not use text-base-content/60 or text-gray-*.
| Context | Class |
|---|---|
| Inside buttons and inline text | size-4 |
| Toolbar, card headers | size-5 |
| Large decorative / empty states | size-8 or size-10 |
Always use ChevronDown with a CSS rotation — never swap between two icon components:
<ChevronDown class="size-4 transition-transform {open ? 'rotate-180' : ''}" />
{#if error}
<div role="alert" class="alert alert-error text-sm">{error}</div>
{/if}
New projects are created from the scaffold with arch.js:
node arch.js create my-app # scaffold only
node arch.js create my-app --modules calendar-events # with modules
node arch.js create my-app --modules commerce,calendar-events # multiple modules
node arch.js list # available modules
node arch.js info calendar-events # module manifest
arch.js create copies candidate/ to projects/<name>/, updates the MongoDB database name, and for each module:
frontend/src/lib/config/nav.tsapi/src/data/permissions.jsondependencies into both package.json files.env and .env.exampleA module is a directory under modules/ containing module.json and optional api/ / frontend/ subtrees that mirror the scaffold layout exactly.
{
"name": "my-feature",
"description": "Short description",
"nav": [{ "label": "My Feature", "href": "/my-feature", "icon": "Star", "permission": "my_feature.read" }],
"permissions": [{ "resource": "my_feature", "actions": ["create", "read", "update", "delete"] }],
"dependencies": { "frontend": {}, "api": {} },
"env": [{ "key": "MY_API_KEY", "default": "", "description": "Required for My Feature" }]
}
Icons must be valid lucide-svelte component names. The permission field is "resource.action" (dot-separated).
| Variable | Default | Notes |
|---|---|---|
SESSION_SECRET |
— | Required. 64-char hex. |
MONGO_DB |
appdb |
|
WEB_PORT |
3000 |
|
API_PORT |
4000 |
|
SMTP_HOST |
(blank) | Blank → Ethereal auto-provisioned in dev |
SMTP_PORT |
587 |
|
SMTP_USER |
— | |
SMTP_PASS |
— | |
SMTP_FROM |
Architectonic <[email protected]> |
|
APP_URL |
http://localhost:3000 |
Used in password reset links |
OLLAMA_URL |
http://host.docker.internal:11434 |
Dev overlay only |
CALENDAR_REMINDER_INTERVAL_MS |
1800000 |
MODULE: calendar-events — reminder scheduler poll interval |
Generate SESSION_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
git config --global core.autocrlf input before cloning.| Package | URL |
|---|---|
| SvelteKit | https://svelte.dev/docs/kit |
| Svelte 5 | https://svelte.dev/docs/svelte |
| DaisyUI v5 | https://daisyui.com/docs |
| Tailwind v4 | https://tailwindcss.com/docs |
| Fastify v5 | https://fastify.dev/docs |
| Nodemailer | https://nodemailer.com |
Build a custom theme: https://daisyui.com/docs/themes/ — set the data-theme attribute in app.html or configure themes in tailwind.config.ts.