A real car-rental SaaS built on @adonisjs-lasagna/*, exercising the whole
platform: two auth realms, schema-per-tenant isolation, the nine satellites
(admin, billing, ai, crypto, sso, backup, websockets, reporting) plus the core
feature set, a telematics plugin, and two Inertia + React consoles (the
platform operator and the rental company).
- Operator lives on the apex host
localhost:3333. - Companies live on a vanity host
<slug>.localhost:3333(e.g.acme.localhost:3333), stored as the tenant'scustom_domain.
A real deployment runs three processes. In dev:
npm run dev— the HTTP server (Vite is auto-started, no--hmrneeded).npm run dev:worker— the queue worker (queue:work). Required for tenant provisioning: company creation dispatches anInstallTenantjob, and the schema only exists once the worker has run it.
Infrastructure (Postgres pgvector/pgvector:pg16 + Redis + MailCatcher) comes
up with npm run infra:up. Ports are 55433 / 56380 / 1025+1080, distinct from
the core demo so both run side by side.
Provisioning is asynchronous, so setup is two passes with the worker running in
between. From apps/rental:
npm run infra:up # Postgres + Redis + MailCatcher
# 1. Control plane: operator account, central car catalog, and the two demo
# companies (each dispatches InstallTenant).
npm run setup # backoffice:setup + central migrate + rental:seed
# 2. Materialise the schemas: start the worker (leave it running) so it drains
# the InstallTenant jobs. The AI provider's after('provision') hook installs
# pgvector into the `extensions` schema as each company is provisioned.
npm run dev:worker # in a second terminal; wait for the jobs to drain
# 3. Data plane: migrate each tenant schema, then fill it with demo data.
npm run setup:demo # tenant:vector:provision + migration:tenant:run + rental:seed:demosetup:demo runs tenant:vector:provision first as a belt-and-suspenders step:
it is idempotent, and it guarantees the vector extension exists on a
pre-existing database (or one whose schemas were provisioned before the AI
provider's hook was in place) before the ai_embeddings vector(N) migration runs.
Then start the server:
npm run dev # http://localhost:3333| Realm | Host | Password | |
|---|---|---|---|
| Operator | localhost:3333 |
operator@karimoto.test |
operator-demo-password |
| Company staff | acme.localhost:3333 |
owner@karimoto.test |
owner-demo-password |
| Company staff | sahara-cars.localhost:3333 |
owner@karimoto.test |
owner-demo-password |
rental:seed(control plane) — the operator account, the shared central car catalog, and the demo company rows (dispatching provisioning). Idempotent.rental:seed:demo(data plane) — fills each already-migrated company with branches, a rate card, a fleet drawn from the catalog, renters with encrypted PII, bookings across the lifecycle (invoices + payments for completed ones), and a small RAG corpus of policy docs whose bodies are embedded into the tenant vector store. Idempotent; safe to re-run to top up missing rows. Sizing follows the company plan (fleet/enterpriseget a full fleet,startera smaller one that stays under itsvehiclesPerTenantquota).
The assistant streams over SSE at POST /ai/chat. Retrieval is opt-in
(retrieve: true): it embeds the query and searches the tenant's ai_embeddings
store, which rental:seed:demo populates from the policy docs. Offline (no
ANTHROPIC_API_KEY) the chat and embeddings run on the in-process mocks, so
retrieval returns real matches but the ranking is a deterministic hash, not
semantic relevance. Set ANTHROPIC_API_KEY (and a real embedding backend) in
.env and the same path uses the real model with no code change — re-run
rental:seed:demo so the corpus is re-embedded into the real vector space.