Architecture
Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16, App Router, Turbopack |
| UI | React 19.2 Server Components, TypeScript, Tailwind v4 |
| Database | node:sqlite: Node 24's built-in SQLite |
| Auth | Clerk hosted sign-in, mapped to a local users row |
pdf-lib, standard fonts only | |
| Validation | zod at action boundaries |
The whole application runs with no external services. No database server, no native modules to compile, no CDN, no web fonts. npm install && npm run dev and it works.
Data access
A thin query layer in src/lib/db.ts exposes all, get and run. Two details matter:
- Rows are re-spread into plain objects.
node:sqlitereturns null-prototype objects, which React Server Components refuse to serialise into Client Components. Fixing it once at the boundary means no caller ever has to think about it. - Schema is applied idempotently on first connection, with an additive migration step for columns introduced after a database was created,
CREATE TABLE IF NOT EXISTScannot add a column to an existing table.
Authorization
Ownership is proven by the query, not by comparing a fetched row. Every scoped read and write includes the owning id in its WHERE clause, so a valid id belonging to someone else simply does not match. There is no separate permission check to forget.
Cross-account viewing, counselor and contributor, funnels through one shared rule in src/lib/access.ts that returns who the viewer is, how they reach the student, and what they may see. The PDF route, the counselor view and the contributor view all consume it, so they cannot disagree.
There is deliberately no middleware. Next.js 16 renamed middleware to proxy and it is the wrong place for auth; every route checks the session in its own layout and every server action re-checks independently.
Mutations
All writes are Server Actions. Each one re-authenticates rather than trusting that the page rendered for the right person, and revalidates the paths it affects.
Multi-row money movement, marketplace purchases, is wrapped in an explicit BEGIN IMMEDIATE / COMMIT with rollback, because a partial write means a student pays for an essay they cannot read.
Design system
Three visual layers over one component set:
- Default: soft, rounded, playful. The student product.
.pro: tighter, flatter, denser. The counselor console..docs: long measure, quiet surface, real typographic hierarchy. These pages.
Colour is expressed as semantic CSS custom properties that flip for dark mode, so components carry no dark: variants at all. Dark mode is opt-in via data-theme="dark" rather than following the OS, because the light palette is the intended look.
Assets
Avatars and university crests are generated inline SVG, no image files, no external requests, no broken images. University crests derive their colours and monogram deterministically from the institution's name via an FNV-1a hash.
Configuration
| Variable | Effect |
|---|---|
CLERK_SECRET_KEY | Clerk server key. Must be set in production. |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Clerk browser key. Publishable by design. |
FLEDGY_DB_PATH | SQLite file location. Defaults to data/fledgy.db. |
RESEND_API_KEY | Enables outbound email. |
FLEDGY_MAIL_FROM | Sender address. Required alongside the API key. |
FLEDGY_BACKUP_DIR | Where nightly backups are written. Defaults to backups/ beside the database. |
FLEDGY_DISABLE_BACKUPS | Set to 1 to skip the nightly backup. Used by CI. |
NEXT_PUBLIC_LOGO_BASE | Self-hosted university logo path. Crests remain the fallback. |
Names from before the rename
The product was Unidash, then Dashly, and is now Fledgy AI. Each rename renamed the variables with it, and every older name is still read as a fallback — FLEDGY_ first, then DASHLY_, then UNIDASH_. This is deliberate: an environment still setting an old name would otherwise boot against an empty database, which presents as total data loss rather than as a misconfiguration.
The same applies to the database file. If fledgy.db does not exist and dashly.db or unidash.db does, it is moved into place on boot, WAL and shared-memory sidecars included, rather than a second empty database being created beside a full one.
Known gaps and limitations are catalogued in Data & accuracy.