Architecture
How Najimi is built and run.
Clients
- Phone and desktop browsers
- Link previews (OG images)
- Docs (this site)
Web
- Next.js 16 App Router
- Partial prerendering
- AI SDK 7 agent
- Better Auth (guests, email codes, Google)
- next-intl, 12 languages
Worker
- pg-boss queues
- Taste reveal
- Image and video jobs
- Chat titles
- Nightly guest sweep
Data
- Postgres 17 (Drizzle)
- RustFS (S3) for photos and creations
- LISTEN/NOTIFY for live events
Outside
- Qloo (taste graph)
- OpenAI (chat)
- fal (images, video)
- ElevenLabs (voice)
The repository
A pnpm and Turborepo monorepo:
| Path | What |
|---|---|
apps/web | The product: pages, API routes, the chat route, cards. |
apps/worker | Every slow job. Runs migrations at start. |
apps/docs | This site (Fumadocs, a static export). |
packages/agent | The model registry, the tools and the rules. |
packages/qloo | The Qloo client: caching, de-duplication, call logging, taste and explore helpers. |
packages/db | Drizzle schema, migrations, data ownership (move a guest's data on sign-in, delete all of a person's data). |
packages/media | Image and video providers behind one interface. |
packages/sources | Safe fetching of outside pages, JSON-LD parsing and a Postgres cache for outside sources. |
packages/storage | S3 objects, signed media links, renditions. |
packages/jobs, live, email, env, config, tokens, ui | Queues, live events, email codes, typed env, every tunable, design tokens, components. |
A chat turn
POST /api/chat
Only the newest message; the server holds the conversation
Context
Person, loves, taste words, photos, mode
streamText + tools
Up to 10 steps; Qloo, making, plans
UI message stream
Cards stream in; saved after every step
- The turn keeps running if the page closes (the stream is drained on the server), and a reload shows it.
- Images and video need an approval in Ask first mode. Approvals are signed, so a client can't forge one.
- The daily allowance is taken atomically in Postgres, with a ceiling for the whole server.
Making things
makeImage
Prompt built from taste words; the person's photo when restyling
media_jobs row
Allowance taken, job queued
Worker
Provider call; renditions (thumb, display) with sharp
Live event
The card swaps its skeleton for the result
Videos are submitted to the provider and finished by a poller, so a restart picks them up again. A paid request is never sent twice; a failed one is refunded.
Live updates
Workers write run events to Postgres and NOTIFY a channel. The web app holds one LISTEN connection and streams events to open pages over server-sent events, replaying any a reconnecting page missed.
Media
Photos and creations live in a private bucket. Every media link goes through the app, which serves it to its owner, to a provider holding a short-lived signed link, to anyone holding a live share token (creations only), or to everyone when public (sample rooms, the showcase).
Running it
Production runs on Coolify: the web app, the worker, Postgres, RustFS and this docs site, each a container. The worker applies migrations when it starts.