Chavica

Developer Docs

Build on top of Chavica.

Architecture, the complete API, and how templates work under the hood — everything in one place.

Next.js 16Prisma 7PostgreSQLGrapeJS EditorCloudinaryBachs

How it works

Architecture

Subdomain routing

middleware.ts inspects the incoming Host header. Registered subdomains and connected custom domains get rewritten to the published-site renderer; everything else falls through to the marketing site and dashboard.

Pages as JSON

Every page — and every template — stores its content as { html, css }. GrapeJS exports and imports directly from this format, so rendering a published page server-side is just interpolating two strings — no editor runtime needed.

Multi-page sites

A site can have many pages. Templates ship with pre-built sub-pages (not blank ones) stored on the template record, copied onto the new site the moment it's created.

Data model

User
 ├─ Site[]            one user, many sites
 ├─ Domain[]          custom domains owned by the user
 ├─ TeamMember[]      teams the user owns or belongs to
 ├─ ApiKey[]          API keys for the v1 API
 └─ Subscription?     current billing subscription (via Bachs)

Site
 ├─ user              owner
 ├─ template?         which Template it was created from (nullable)
 ├─ Page[]            every page on the site (home + sub-pages)
 ├─ Domain[]          custom domains connected to this site
 ├─ pageViews         analytics events
 └─ published         whether it's live on its subdomain

Page
 ├─ site
 ├─ slug, title
 ├─ content   Json    { html, css } — GrapeJS output
 └─ isHome    Boolean — exactly one page per site is home

Template
 ├─ content   Json    { html, css } — the home page
 ├─ pages     Json?   [{ slug, title, content: { html, css } }, ...]
 ├─ isPremium          gates FREE-plan users
 └─ hidden             soft-delete; hidden templates skip the picker

Editor → Save → Publish lifecycle

  1. 1

    The editor loads all Pages for a site and opens the home page in GrapeJS.

  2. 2

    Template CSS is injected directly into the canvas iframe's <head> on load — not via a data: URI <link>, since Chrome silently blocks those.

  3. 3

    On save, the editor returns { html, css }, PATCHed to /api/sites/[siteId]/pages/[pageId].

  4. 4

    Switching page tabs auto-saves the current page first, then loads the next page's content into the same GrapeJS instance.

  5. 5

    Publishing sets Site.published = true. The editor shows a two-phase modal: a loading state while the request is in flight, then a live-URL confirmation once it resolves.

  6. 6

    Visiting subdomain.chavica.com renders the home Page; subdomain.chavica.com/any-slug renders the matching non-home Page — both require published === true or they 404.

Billing & payments (Bachs)

All money flows through Bachs — chosen for cross-border collection (card, bank transfer, mobile money, crypto in NGN/USD + multi-currency). The flow is redirect-based, not a client-side popup, which changes where the business logic lives:

  1. 1

    A route (billing/subscribe, icons/unlock, domains/purchase) creates a Bachs checkout with an amount and a metadata object tagging what's being purchased. Nothing is provisioned yet — it just returns a checkoutUrl.

  2. 2

    The client redirects the whole browser to that hosted checkout page. There's no inline JS popup.

  3. 3

    The webhook is the only reliable signal that payment succeeded — success_url/cancel_url redirects are UX only and can land back on Chavica before the webhook arrives.

  4. 4

    /api/webhooks/bachs verifies the signature, checks a WebhookEvent table for idempotency (Bachs guarantees at-least-once delivery), then dispatches on metadata.type to actually upgrade the plan, unlock the icon pack, or register the domain.

  5. 5

    The page that triggered checkout polls router.refresh() a few times after redirect, since the webhook may land a moment later.

Reference

Full API reference

Every route under app/api. Unless noted, routes require the chavica_token session cookie — getCurrentUser() returns 401 if it's missing.

Auth

/api/auth — sets/clears the chavica_token session cookie

POST/auth/registerCreate a user, sets session cookie
POST/auth/loginVerify password, sets session cookie
POST/auth/logoutClears session cookie
GET/auth/meCurrent user info
POST/auth/change-passwordChange password (requires current password)
POST/auth/forgot-passwordSends a reset link/token
GET/POST/auth/reset-passwordValidate reset token / set new password

Sites & Pages

/api/sites — requires an authenticated session

GET/sitesList the current user's sites
POST/sitesCreate a site. Body: { name, templateId?, subdomain? }
GET/sites/[siteId]Fetch one site (must be owner)
PATCH/sites/[siteId]Update name, favicon, metaTitle, metaDesc
DELETE/sites/[siteId]Delete a site, cascades pages/domains
GET/sites/[siteId]/analyticsPage view counts for the site
POST/sites/[siteId]/publishSet published = true
DELETE/sites/[siteId]/publishSet published = false
GET/sites/[siteId]/pagesList pages on a site
POST/sites/[siteId]/pagesCreate a blank page. Body: { slug, title }
GET/sites/[siteId]/pages/[pageId]Fetch one page
PATCH/sites/[siteId]/pages/[pageId]Save editor content. Body: { content: { html, css } }
DELETE/sites/[siteId]/pages/[pageId]Delete a page (not the home page)

Templates

/api/templates

GET/templatesList visible templates (hidden: false) for the picker

Domains

/api/domains

GET/domains/search?q=Check domain availability
POST/domains/purchaseCreate a Bachs checkout for a domain, returns { checkoutUrl }
POST/domains/[domainId]/connectConnect a purchased domain to a site
GET/resolve-domain?host=Maps an incoming Host header to a Site (used by middleware)

Billing

/api/billing — Bachs-backed, cross-border

POST/billing/subscribeCreate a Bachs checkout for a plan, returns { checkoutUrl }
POST/billing/cancelCancel the active subscription (local-only)
PATCH/billing/white-labelUpdate brandName, brandLogo, brandColor

Team & API Keys

/api/team and /api/api-keys

GET/teamList team members the user owns or belongs to
POST/teamInvite a team member by email
DELETE/team/[memberId]Remove a team member
GET/api-keysList the user's API keys
POST/api-keysIssue a new API key
DELETE/api-keys/[keyId]Revoke a key

Public v1 API

/api/v1 — authenticate with an API key, not a session cookie

GET/POST/v1/sitesList / create sites programmatically
GET/v1/sites/[siteId]Fetch one site
PATCH/v1/sites/[siteId]Update one site
DELETE/v1/sites/[siteId]Delete one site
GET/v1/analyticsAnalytics for the key owner's sites

Icon Packs

/api/icons

GET/icons/statusWhich icon packs the user has unlocked
POST/icons/unlockCreate a Bachs checkout for a premium icon pack, returns { checkoutUrl }

Misc

POST/uploadCloudinary image upload, used inside the editor
POST/contact/[siteId]Public contact form submission for a published site
POST/webhooks/bachsVerifies X-Bachs-Signature, handles collection.succeeded/failed/abandoned/underpaid — source of truth for billing

Admin

/api/admin — requires role ADMIN or SUPER_ADMIN

PATCH/DELETE/admin/sites/[siteId]Force-edit or delete any user's site
PATCH/DELETE/admin/users/[userId]Edit role/plan or delete any user
GET/POST/admin/templatesList all templates (incl. hidden) / create a new one
PATCH/admin/templates/[templateId]Empty body toggles hidden; full body updates any field incl. pages
DELETE/admin/templates/[templateId]Permanently delete a template

Templates

How templates are built

Every template is a { html, css } home page plus a pages array of pre-built sub-pages — never blank scaffolding. The 9 shipped templates:

TemplateCategoryPlanPre-built sub-pages
NebulaLanding (SaaS)Free/about, /contact
Bella VistaRestaurantPro/menu, /reservations
MonoPortfolioFree/contact
SolarAgencyPro/work, /about, /contact
Kemi's BoutiqueStorePro/shop, /lookbook, /contact
QuillBlogFree/articles, /about, /newsletter
CatalystBusinessPro/about, /contact
VertexBusiness (coach)Pro/about, /contact

Three legacy templates (Portfolio, SaaS Landing Page, Restaurant) exist in the database using an old GrapeJS content format the current renderer can't read — they're permanently hidden and replaced by Mono, Nebula, and Bella Vista above.

Seeding scripts

# Creates/updates each template's home page

npx tsx prisma/seed.ts

# Builds and attaches every pre-built sub-page

npx tsx prisma/seed-pages.ts

Adding a new template

  1. 1

    Add it to prisma/seed.ts with a unique id, name, category, content, and isPremium flag, then run npx tsx prisma/seed.ts.

  2. 2

    If it needs sub-pages, add a block to prisma/seed-pages.ts following the existing pattern, then run npx tsx prisma/seed-pages.ts.

  3. 3

    Or skip code entirely: open the admin panel's Templates tab and use "Manage Pages" on any template to add/edit/remove sub-pages through a UI.

Good to know

Template previews

Template card thumbnails and the full-screen preview modal both render the actual template HTML/CSS in a sandboxed iframe (the card scales it to 0.3x) — what you see is exactly what you get, not a static screenshot. Both inject a script that intercepts every click on a link or submit button, so clicking inside a preview never navigates away from the page you're on.