I have been building and rebuilding the same foundation for every new web project: auth, roles, messaging, notifications, settings. The stack is always the same — SvelteKit on the front, Fastify on the back, MongoDB for storage. The work is never interesting and it is never quite right the first time. Architectonic is the version I stopped redoing.

The core idea is simple: the candidate/ directory inside the repo is the scaffold. You do not configure it, you do not install a generator — you clone it. One command produces a running full-stack application with a real database, a real session layer, and a real permission system. Everything after that is your product.

The clone model

The scaffold lives under candidate/. To start a new project you run:

node arch.js create <name> [--modules a,b] [--no-install]

This copies the scaffold to projects/<name>/, updates the MongoDB database name, merges each requested module, and runs npm install. The result is a fully wired project — not a template with TODOs, not a starter that assumes you will fill in the hard parts. The hard parts are already there.

The candidate is the source of truth for the scaffold. If the scaffold gets better — new patterns, new utilities, updated dependencies — the candidate is what changes. Projects that already exist keep their copy. There is no update mechanism to manage, no breaking change to coordinate. Each project is independent from the moment it is created.

The stack

The stack is chosen for durability and for the specific shape of the problem: server-rendered pages with real-time capability, a typed API, and a document store that does not fight the data model.

  • SvelteKit 2 with Svelte 5 runes — server-side rendering, file-based routing, and a compiler model that keeps the client bundle small. The runes API ($state, $derived, $effect, $props) replaces stores with a simpler mental model. The BFF pattern — SvelteKit server routes forwarding to the Fastify API — keeps the session in one place and eliminates cross-origin CORS entirely.
  • Fastify 5 — schema-validated routes, plugin-based architecture, and a preHandler hook model that makes auth and permission checks composable. The @fastify/autoload plugin discovers route files by directory, so adding an API resource is dropping a file, not registering a route.
  • MongoDB 7 — flexible schema for a data model that is still being discovered. Three collections cover 80% of needs in a new product: users, roles, settings. Everything else grows as needed.
  • TypeScript everywherecomposite: true, project references, a shared tsconfig.base.json. The frontend and API share nothing at runtime but agree on types through the schema package.
  • Tailwind v4 + DaisyUI v5 — utility classes for layout and spacing, DaisyUI for interactive components. Theme toggle (dark/light) is wired and persisted to localStorage out of the box.

What ships in the scaffold

The scaffold is not a "hello world." It ships every piece of infrastructure a real product needs before it can build features:

Auth — session-based login and logout via @fastify/session. Password hashing with bcrypt (12 rounds). Full forgot-password → email token → reset-password flow using Nodemailer. In development, Ethereal auto-provisions a catch-all email account and logs a preview URL — no SMTP config required.

RBAC — three collections: users, roles, permissions. Roles are named groups of permission strings following a resource:action grammar. A single preHandler hook resolves the user's effective permission set once per request and stashes it on request.permissions. Route guards are one-liners. The 403 returns the missing permission string — "Forbidden, you need billing:refund" turns a support ticket into a one-line role update.

Messaging — full in-app email-style messaging. Thread model, replies, per-user state (read/archived/deleted), Tiptap rich-text editor, unread badge in the header. The compose page has a recipient chip picker with autocomplete.

Real-time notifications — WebSocket push via @fastify/websocket. Persistent storage with a 90-day TTL, groupKey deduplication so repeated events update in place rather than flooding. The app.notify() Fastify decorator lets any route module push a notification in one line without importing the internals. Quiet hours and per-type mute preferences ship in the settings page.

Teams — named groups of users with full CRUD and member management. Team assignment is available on any resource that needs it. The profile page shows each user's teams. Permissions are scoped: owner/admin get full CRUD, lead gets read + update, contributor/viewer get read only.

Audit log — a capped audit_logs collection records 14 events across auth, user management, roles, and messaging. Every entry captures userId, username, action, resourceId, IP, and timestamp. The admin view filters by user and date range. If you cannot answer "who changed this?" in thirty seconds, you do not have RBAC — you have a wishlist.

Settings — admin-only key/value configuration backed by MongoDB. Three defaults ship on boot: app name, registration open/closed, theme mode. The seed uses $setOnInsert on the value field so user-edited settings survive restarts.

Chat assistant — Ollama-backed, fixed bottom-right panel. Connects to the host machine via host.docker.internal:11434. Drop in any Ollama model. The store is module-level $state and persists across navigation.

The module system

Beyond the scaffold core, Architectonic has a build-time module system. Modules live under modules/. Each module is a self-contained folder with API routes, frontend pages, a nav manifest, and declared permissions. There is no runtime plugin system — modules are baked in at build time via arch.js create.

Five merge points handle the integration automatically: API routes (dropped into api/src/routes/ for autoload), nav entries (appended to nav.ts), permissions (merged into the seed defaults), package dependencies (deep-merged into both package.json files), and environment variables (appended to .env if the key is absent). Collision detection walks both source trees before writing any files and fails fast with a clear error.

Current modules: commerce (storefront + dashboard + analytics), agile (sprint tracker), CRM (deal pipeline), notifications (base), and several others in progress. Enabling a module is a one-line uncomment in two registry files and a container restart. Disabling it is the same operation in reverse — no migrations, no deletions.

Local dev

One command: docker compose -f docker-compose.yml -f docker-compose.dev.yml up. Three services — web, api, mongo. Source is bind-mounted. On Windows, file events do not propagate reliably through bind mounts, so the dev workflow is explicit rebuilds rather than hot reload. The compose file is the single source of truth for local environment variables — no scattered .env files.

The boring foundation earns its keep by being the foundation you don't have to rebuild. Every hour not spent on auth is an hour spent on the product.