20 KiB
Agent Instructions
Stack
- Runtime: Bun - not Node. Use
buncommands, nevernpm/npx. - Framework:
@moduletrace/bunext- not Next.js. Routing is filesystem-based undersrc/pages/, served byBun.serveinsrc/server.ts. - Database:
@moduletrace/bun-sqlite- SQLite database. Schema indb/schema.ts, generated types indb/types/db.ts. Config inbun-sqlite.config.ts. - UI: Tailwind CSS v4 + the
@moduletrace/twuicomponent library (installed package) + shared components insrc/components/general/. There is no shadcn/ui, notailwind.config, and nocomponents.json- twui is the only component system. CSS chain:src/pages/__root.tsximportssrc/styles/main.css, which imports@moduletrace/twui/base.css(@import "tailwindcss"). Theme tokens (colors/radius/fonts) are overridden via the@theme inlineblock insrc/styles/main.css. - Icons:
lucide-react. - Validation:
zodv4 ([email protected]). - Branding: Site name/slug live only in
src/data/site-data.ts. Logo source of truth ispublic/logos/logo.svg(rendered inline bysrc/components/general/logo-icon.tsx); browser favicon/assets are generated from it.
Entrypoint
src/server.ts - runs bunextInit(), starts Bun.serve, then starts cron:
/media/*short-circuits to the media server handler/wsupgrades only afteruserAuthsucceeds (session frozen into the socket at upgrade time); thewebsockethandler object lives insrc/websocket/- everything else goes to
bunext.bunextRequestHandler cron()starts after the server is up; in production it is gated to theMAIN_SERVERcontainer
Commands
| Purpose | Command |
|---|---|
| Dev server | bun run dev (runs bun src/server.ts) |
| Production | bun run start (NODE_ENV=production bun src/server.ts) |
| Type-check only | bun run typecheck |
| Run tests | bun run test |
| Regenerate favicon/brand assets | bun run gen:favicon (runs src/scripts/generate-brand-assets.ts) |
| DB schema (tables) | bun run db:schema |
| DB admin UI | bun run db:admin |
| DB backup | bun run db:backup |
| Seed super admin user | bun run seed (runs src/scripts/seed-super-admin.tsx) |
| Query DB (CLI) | bun llm-code-assistant/query-db.ts "<sql>" |
| Install | bun install |
No linter or formatter configured. bun run typecheck and bun run test are the verification steps. Tests are colocated .test.ts/.test.tsx files - run one with bun test <file>.
Querying the Database
Inspect live data with bun llm-code-assistant/query-db.ts "<sql>" - a read-only SQL CLI that loads the schema from db/schema.ts automatically and prints a normalized JSON envelope (success, payload, count, insert_return, error, msg). Exit code 1 on failure. Use it instead of fishing through ORM code when you need to know what's actually in the database.
Path Alias
@/* resolves from repo root. Import like @moduletrace/twui/layout/Button, @/src/components/general/logo, @/src/data/site-data, @/db/types/db.
Architecture
Auth
- Auth guard:
src/pages/__root.server.ts- server function that runs on every request.- Redirects unauthenticated
/admin/*→/auth/login - Already-authenticated users hitting
/auth/*(except paths containing/logout) are redirected to/admin
- Redirects unauthenticated
- Session: encrypted auth cookie (AES-256-GCM, PBKDF2-derived key from
ENCRYPTION_KEY/ENCRYPTION_SALT) plus a CSRF cookie.userAuth(src/functions/backend/auth/user-auth.ts) decrypts, checks expiry (AuthExpiryDays, 60) against the embeddeddate, verifies the CSRF cookie against the token embedded in the session (constant-time,src/utils/safe-compare.ts), and re-readsuser_typesfrom the DB every request (revoked users lose access immediately). - Login: SSO codes (
sso_login_codestable, 15-min expiry,randomInt-generated) viaapi/auth/get-login-code.tsx+api/auth/login.ts. Codes are bound to theuser_idin the encrypted SSO cookie, compared withsafeCompare, and deleted afterSSOMaxCodeAttemptsfailures.get-login-codereturns the same response for unknown users (no enumeration). Google OAuth is available viaapi/auth/google-login.ts(creates the account when called withgoogle_signupfrom the signup page). Sessions are issued bysrc/functions/backend/auth/login-user.ts(supportsreauthfor silent refresh without audit logging). - Login method (site-wide,
AUTH_LOGIN_METHOD):code(default; passwordless, emailed code only viaget-login-code) orpassword(api/auth/password-login.ts, then the emailed code as a second step). Parsed bysrc/utils/parse-login-method.ts(anything butpasswordmeanscode), read server-side withgrabLoginMethod()and client-side withuseLoginMethod()(fromenvs.AUTH_LOGIN_METHOD). Each mode's endpoints refuse requests in the other mode. The SSO cookie records the issuingmethod;verifySSOCode(src/functions/backend/auth/verify-sso-code.ts) only accepts codes whose method matches, so login takes the site method and password reset takesreset. Password mode adds/auth/reset-password(+/confirm) viaapi/auth/request-password-reset.ts/api/auth/reset-password.ts, a required password at signup, and set/change password in/admin/settings/password. bcrypt hashes live in the separateuser_passwordstable (never inusers; blocked from the CRUD API viasrc/dict/private-tables.ts).api/auth/resend-login-code.tsresends from the SSO cookie. - Auth layout:
/auth/*(except/auth/logout) renders insrc/layouts/auth/with no site header/footer. - RBAC:
src/utils/check-user-access.ts(includes/excludes primitive,super_adminbypass) plus the declarative engine insrc/utils/rbac/-userTypeAssignmentinterpretsUserTypeAssignmentMatrix/UserTypeDeletionMatrixinsrc/dict/user-types-dict.ts.read_onlyusers are blocked from non-GET CRUD in the router. - Audit trail:
src/functions/backend/logger.ts-logEvent/logErrorwrite to thelogstable. Login, SSO, Google login, logout, cron runs, and WebSocket connects are all logged.
WebSocket
- Upgrades on
/wswithuserAuthvalidation (401 if not logged in);WebSocketSession(src/websocket/types.ts) carries{ user, user_types }. - Rooms declared in
src/dict/websocket-rooms.ts-> typedWebsocketRoomType. Store insrc/websocket/(data)/rooms.ts:WeakMapfor per-client metadata,Map<room, Set<ws>>for lookups, empty rooms deleted. Join/leave/broadcast insrc/websocket/(functions)/. - Message dispatch in
handle-message.ts:client:ping→server:pong, unknown events →error. The twuiuseWebSockethook sends a raw"twui::ping"keepalive string - non-JSON messages are ignored. - Client handlers:
src/websocket/client-requests/<event>/index.ts(folder-per-event), replying throughwsSendData.
Cron
src/cron/index.ts is the single registration point called from server.ts. Tasks live in one folder each (src/cron/<name>/index.ts) as default-exported async functions, scheduled with in-process Bun.cron via registerCronJob({ title, schedule, task, run_on_start }) (src/cron/register-cron-job.ts), which logs before each run and catches/logs errors. Schedules are cron expressions or nicknames (@hourly) interpreted in UTC; runs never overlap. Gated in production by MAIN_SERVER. The bundled heartbeat task doubles as a DB-liveness probe.
Admin CRUD API
api/admin/crud/[[...paths]].ts dispatches GET/POST/PUT/DELETE by table name: userAuth → read_only GET-only gate → per-table switch → generic fallback (super admin only). Tables without custom handlers fall through to generic CRUD with zero code. Customize a table by adding src/pages/api/admin/crud/(tables)/<table>/ with index.ts + per-method files, then add a case in the router.
Documents (Letters / Emails / Invoices / Quotes)
The preset ships a complete document suite on top of the generic CRUD:
- Tables:
letters,emails,email_attachments,invoices,invoice_items,quotes,quote_items,invoice_quotes(statuses/enums insrc/dict/document-dict.ts) - Admin sections:
/admin/letters,/admin/emails,/admin/invoices,/admin/quotes- full list/create/view/edit CRUD, line items, quote-to-invoice linking, PDF download, send-email modals - PDF:
src/functions/backend/pdf/-pdf-helpers.ts,letter-pdf.ts,invoice-pdf.ts(drives both invoices and quotes viakind),render-blocks.ts+html-blocks.ts(TinyMCE HTML → PDF blocks),letter-date.ts - Email:
src/functions/backend/email/-email-client.ts(nodemailer SMTP),send-email.tsx(renders ReactNode into thesrc/email/root.tsxshell),document-emails.tsx(invoice/quote bodies) - API routes:
api/admin/{letters,invoices,quotes}/generate-pdf.ts(streams PDF binary),api/admin/{emails,invoices,quotes}/send-email.tsx,api/admin/emails/sync-attachments.ts - DB grabs:
db/documents/,db/invoices/grab-invoice.ts,db/quotes/grab-quote.ts - Client helpers:
src/functions/frontend/document-actions.ts(PDF download + send-email actions) - Numbering: invoice/quote numbers auto-generate as
<SLUG>-YYYYMM-###/<SLUG>Q-YYYYMM-###fromSiteData["SiteSlug"]in theinvoices/quotesCRUD handlers - Shared UI:
src/components/general/pdf-preview.tsx,sortable-line-items-editor.tsx(+sortable-line-item-row.tsx),copy-from-existing-select.tsx,main-hero-section.tsx
Admin Route Hierarchy
Actual preset routes under /admin:
/admin- dashboard/admin/people- admin users list, pluscreate/,[user_id]/(profile),[user_id]/edit//admin/letters- letter templates, pluscreate/,[id]/,[id]/edit//admin/emails- email records/drafts, pluscreate/,[id]/,[id]/edit/,template/(live email-shell preview)/admin/invoices- invoices, pluscreate/,[id]/,[id]/edit//admin/quotes- quotes, pluscreate/,[id]/,[id]/edit//admin/backups- super admin only: create/list/download database snapshots indb/.backups, plusrestore/(restore a listed backup, or upload a SQLite database file which is saved to.backupsthen restored). Shells out to thebun-sqliteCLI (backup, andimport <file>sincerestoreis interactive) viasrc/functions/backend/backups/; API routes inapi/admin/backups//admin/settings- current user profile, pluspassword/(only whenAUTH_LOGIN_METHOD=password; redirects to/admin/settingsotherwise). The aside renders it as a collapsible sub-nav viachildrenin(dict)/admin-aside-groups-dict.tsx
Admin sidebar data in src/layouts/admin/(partials)/admin-aside-links-dict.tsx; aside chrome in (partials)/admin-aside.tsx. Page heroes use the shared AdminHero component (src/components/general/admin-hero.tsx).
Page Directory Rules
For route-local code under src/pages, use non-routed meta folders:
(sections)- page sections(partials)- components used by sections(data)- page-local data(functions)- page-local functions(utils)- page-local utilities(hooks)- page-local hooks
Keep route files (e.g. index.tsx) thin - compose from route-local meta folders.
Dynamic routes use [param] syntax (e.g. [id], [user_type], [key]). Catch-all routes use [[...paths]].
Server functions use index.server.ts alongside index.tsx (e.g. single-document/[id]/index.server.ts).
Shared Code Organization
src/hooks/- Shared React hooks:use-admin-crud-get,use-fetch-get,use-form-init,use-local-user,use-users-getsrc/functions/- Shared functions split by target:src/functions/backend/- Server-only: auth (backend/auth/), DB queries (backend/db/incl.documents/,invoices/,quotes/), email client + templates (email/), PDF generation (pdf/), media upload,logger.ts(audit logging)src/functions/frontend/- Client-safe: CRUD handlers, fetch handlers, media upload/delete handlers,document-actions.ts(PDF download/send actions)src/functions/server/- Server utilities (media server handler etc.)
src/utils/- Utility modules: cookies, crypt, ejson, money, safe-compare, text-to-html, and more. Most have corresponding.test.tsfiles.src/utils/rbac/- RBAC engine:user-type-assignment(matrix interpreter),is-pass-through-user,can-*permission helpers,filter-assignable-user-types- withbun:testsuites driven by fixture matricessrc/dict/- Dictionaries/constants: main links (main-links-dict.ts), user types + assignment matrices, media types, document statuses (document-dict.ts), websocket rooms, log categories, company datasrc/data/- Static data:site-data.ts(site name/slug - the only place brand literals live),app-data.ts(app constants - cookie names, limits, defaults, Paystack endpoint)src/types/- Type definitions:index.ts(incl. WebSocket event/room types),sql-joins.ts,paystack.ts,search-api.tssrc/scripts/- CLI scripts:seed-super-admin.tsx(idempotent super admin seed) +generate-brand-assets.ts(favicon/brand asset generator)llm-code-assistant/-query-db.ts(read-only SQL CLI for coding assistants)
Component Systems
| Directory | Description |
|---|---|
@moduletrace/twui |
The design system - external package. Contains layout primitives, elements, editors (TinyMCE), form components, hooks, SVGs, and utilities. Import from @moduletrace/twui or a deep path such as @moduletrace/twui/layout/Button |
src/components/general/ |
Shared page components built on twui: logo, logo-icon, inner-page-hero-section, main-hero-section, admin-hero, login-shell, tagline-section, pdf-preview, sortable-line-items-editor, copy-from-existing-select, media/image helpers |
UI & Styling Rules (MANDATORY)
- Always build UI with the twui component library and the shared
src/components/general/components. Do not hand-roll HTML for layout, typography, or controls.- Layout:
Section,Container,Stack,Row,Center,Span,P,H1-H5,Button,Divider,Img,Link,ArrowedLink,Main,Header,Footer,Spacer - Elements:
Card,Paper,Tag,HeaderNav(+MainLinksdict),LinkList,Tabs,Table,Breadcrumbs,Modal,Dropdown,CheckBulletPoints,EmptyContent,Loading,LoadingBlock,Toggle,Toast - Forms:
Form,Input,Select,Textarea,Checkbox,Radios,SearchSelect,FileUpload,ImageUpload - Shared:
Logo,LogoIcon,InnerPageHeroSection,AdminHero,LoginShell(auth pages/forms),TaglineSection
- Layout:
- UNCHANGEABLE RULE - twui components always win over raw JSX elements. Use twui components (
H1-H5,P,Span,List,Img,Row,Stack,Container,Section,Card,Button,Tag,Link, etc.) before basic JSX elements (<h1>-<h6>,<p>,<span>,<ul>,<li>,<img>,<a>,<button>,<div>-as-card, etc.). Never introduce or reintroduce a raw JSX element where a twui equivalent exists. Override twui defaults viaclassNamerather than reaching for the raw element. - Never create new CSS classes, CSS files, or inline
style={{...}}hacks. Style exclusively by composing existing component props/classNames, existing twui classNames (twui-*), semantic theme-token utilities (bg-dark,bg-light,text-dark,text-primary,bg-primary,text-accent,bg-accent,text-secondary,border-slate-200, etc.), and utility classes already used elsewhere in this codebase. Build up from what exists; extend component code only when the existing API cannot express the need. - Theming: colors, radii, and fonts change only in two places: the
@theme inlineblock insrc/styles/main.css(overrides@moduletrace/twui/base.cssdefaults). Never hardcode hex colors, radii, or font stacks inside components. - Branding:
public/logos/logo.svgis the single source of truth for the logo mark;logo-icon.tsxrenders it inline andlogo.tsxrenders the wordmark fromSiteData["SiteName"]. Whenever the logo changes, runbun run gen:faviconto regeneratepublic/favicon.ico,public/favicon.png,public/apple-touch-icon.png, andpublic/logos/logo.png(the last is embedded in generated PDFs). Font + favicon links live in__root.tsxHead(). - Site name: brand literals live only in
src/data/site-data.ts. Page meta titles, emails, and UI copy must interpolateSiteData["SiteName"]- never hardcode a brand name in page files.
Conventions
- Prefer one function/component per file over monolithic files with multiple exports.
- Modularization is non-negotiable: prioritize modularization over monolithic functions/components. Repeated UI blocks must be extracted into their own reusable components driven by props (e.g. an info row component taking
{ icon, text }), never repeated inline markup. - Every rendered array must have a separate component for each array item. Always extract the item JSX into its own component and render it via
items.map((item) => <ItemComponent {...item} />)- never inline the item's JSX inside the map callback. - No em dashes (U+2014) anywhere in the repo: code, comments, docs, markdown, or user-facing strings. Use a regular hyphen (
-) or restructure the sentence instead. - Always use object parameters (
{ foo, bar }) instead of comma-separated positional parameters. - Prefer existing patterns over new abstractions.
- Do not patch
@moduletrace/twui- it is a general, publishable library shared across projects. Do not edit the installed package innode_modules; compose or override withsrc/components/general/components andsrc/styles/main.cssinstead. .envis gitignored but exists locally with secrets. Do not commit it. It is also excluded by.dockerignore; production passes it withdocker run --env-file.- Rate limiting:
src/utils/rate-limit.ts(in-memory fixed window, per process). Unauthenticated endpoints that send email or check secrets (get-login-code,login,contact) must be rate limited. - Validation: validate untrusted request bodies with the zod schemas in
src/schemas/viaparseWithSchema. ThrowAppErrorfor messages that are safe to show; catch blocks returnsafeErrorMessageso internal errors never reach the client. - HTML columns: any
html: truecolumn that accepts user input must be sanitized withsanitizeRichTextHtml(orsanitizeUserBioforusers.bio) before it is stored or rendered withdangerouslySetInnerHTML. - Outgoing email outside production goes through
resolveEmailRecipient(src/functions/backend/email/resolve-email-recipient.ts) and is redirected toDEV_EMAIL_RECIPIENT. Never hardcode recipient addresses. - Never trust ids from the request body for ownership or sessions. Use the session
user.id, and gate acting on other users with thesrc/utils/rbac/helpers (canUpdateUseretc.). secrets/directory is also gitignored.external/binary directories (**/models,**/bin,**/x-86,**/arm-64) are gitignored.