Architecture Diagram
This document gives a high-level view of how Tours fits together: the browser, the single Next.js application (pages and API), the PostgreSQL database, object storage, the license server, and integration providers. Tours is one deployable Next.js 16 app — there is no separate API server, no ORM, and no message broker.
System Diagram
graph TD
B["Browser · Public site · Admin · Traveler · Operator · Influencer · Installer"]
B -->|"HTTPS (page loads + fetch /api/v1/*)"| APP
subgraph APP["Next.js 16 App (App Router · React 19)"]
SITE["(site) — public site + storefront + booking"]
AUTH["(auth) — traveler / operator auth"]
ADMIN["admin/(panel) — cookie: admin_token"]
CUST["customer — traveler portal · member_session"]
OPER["operator — operator portal · member_session"]
INFL["influencer — affiliate portal"]
INST["install — first-run wizard"]
API["api/v1/* — ROUTE HANDLERS = the API"]
LIB["src/lib — db · auth · r2 · secrets · license"]
CORE["src/core + src/product — channels · plugins · payments · adapters (63 add-ons)"]
end
API -->|"ensure*Schema() → auth guard → parameterized SQL"| DB[("PostgreSQL — Neon serverless / local Postgres")]
API -->|"S3 API"| R2[("Cloudflare R2 / S3 — uploads, media")]
APP -->|"offline RS256 verify + heartbeat"| LIC["License server (CreativeCape)"]
API -->|"via add-ons"| INTG["Integration providers — payments · email · sms · video · analytics · captcha · chat"]
Client
The browser is served five experiences from the same application, separated by App Router route groups and gated by their respective cookies:
- Public site / storefront (
src/app/(site)) — home, tour catalog, tour detail, destinations, blog, CMS pages, booking / checkout. - Admin panel (
src/app/admin/(panel)) — dashboard, tours, bookings, customers, operators, inclusions/extras, reviews, blogs, marketing, settings, apps. Gated by theadmin_tokencookie. - Traveler portal (
src/app/customer) and Operator portal (src/app/operator) — gated by themember_sessioncookie; the operator is resolved from a linkedhostsrow (getOperatorId()). - Influencer portal (
src/app/influencer) and the installer (src/app/install).
There is no separate single-page app in this repository; all UI is rendered by Next.js
server and client components, and all data access goes through /api/v1/* (which also
makes the same surface available to mobile clients via Authorization: Bearer).
API
The API is route handlers, not a standalone server. Every route.ts under
src/app/api is an endpoint; the repository contains roughly 300 of them. The primary
surface is versioned under /api/v1, with /api/install and /api/license outside the
version prefix and an admin-gated OpenAPI document at /api/openapi.json (rendered via
Scalar at /api-docs).
Representative /api/v1 resource segments:
admin · analytics · blogs · booking-settings · bookings · brand · calendar-config ·
captcha · chat · chatbot · countries · coupons · cron · currencies · customer ·
destinations · extras · faqs · forms · gallery · hero-sliders · i18n · ical ·
inclusions · influencer · languages · livechat · locations · operator · pages ·
plugins · portal · reviews · testimonials · tours · track · video
Each handler typically (1) calls the relevant ensure*Schema() function, (2) checks
auth first (getSession() for admin, getCustomerId() / getOperatorId() for portals)
plus per-route permissions on admin routes, (3) re-validates client input, (4) runs
parameterized SQL, and (5) returns JSON via ok() / err().
Layer Diagram — Core vs Product
The extensibility system splits stable framework code (src/core) from swappable add-ons
(src/product). Add-ons are aggregated at build time into generated index files,
which the core registries import.
┌──────────────────────── src/core (stable contracts + registries) ───────────────────────┐
│ │
│ channels/registry.ts → CHANNELS = mergeChannels( │
│ email (FREE: SMTP), payments (FREE: COD/Bank/PayPal), storage (FREE: S3/R2/GCS), │
│ analytics (FREE: GA4), video (FREE: YouTube/Vimeo), …ADDON_CHANNELS ) │
│ │
│ plugins/registry.ts → FEATURES = [ …ADDON_FEATURES ] (FeaturePlugin contract: │
│ nav · admin/portal/public pages · api · hooks · ensureSchema · settings) │
│ │
│ payments/registry.ts → PAYMENT_DRIVERS = [ cod, bank, paypal, …serverAdapters(payment)]│
│ │
│ adapters/client.ts·server.ts → clientAdapters(kind) / serverAdapters(kind) │
│ resolve a provider implementation by kind + provider at runtime │
└───────────────────────────────────────────▲────────────────────────────────────────────┘
│ imports the GENERATED indexes
┌────────────────────── src/product (add-ons + generated aggregation) ─────────────────────┐
│ │
│ addons/<id>/ one folder each addon.json + index.ts (+ schema.ts / api.ts / *.tsx)│
│ feature add-ons → announcements, audit-logs, webhooks, digest, maps, … │
│ provider add-ons → payments-mollie, payments-paystack, email-sendgrid, │
│ analytics-clarity, chatbot-anthropic, calendar-google_calendar, … │
│ │
│ addons/index.ts [GENERATED] → ADDON_CHANNELS │
│ features/index.ts [GENERATED] → ADDON_FEATURES │
│ adapters-server/index.ts [GENERATED] → ADDON_SERVER_ADAPTERS (by kind) │
│ adapters-client/index.ts [GENERATED] → ADDON_CLIENT_ADAPTERS (by kind) │
│ (produced by scripts/bundle-addons.mjs — do not hand-edit) │
└──────────────────────────────────────────────────────────────────────────────────────────┘
The same relationships as a Mermaid graph:
graph TD
B[Browser] -->|HTTPS| APP[Next.js 16 App]
subgraph APP[Next.js 16 App Router]
SITE["(site) storefront"]
ADMIN["admin/(panel)"]
CUST[traveler portal]
OPER[operator portal]
API["api/v1/* route handlers"]
CORE[src/core registries]
PROD[src/product add-ons]
end
PROD -->|generated indexes| CORE
API -->|parameterized SQL| DB[("PostgreSQL / Neon")]
API -->|S3 SDK| R2[("Cloudflare R2 / S3")]
APP -->|offline RS256 verify + heartbeat| LIC[License server]
API -->|via add-ons| INTG["Payments / Email / SMS / Video / Analytics"]
Database
- Engine: PostgreSQL. In production it is hosted on Neon and reached through
@neondatabase/serverless(HTTP driver); in local development a vanilla Postgres is reached over the wire protocol viapostgres.js. Both are exposed through the samesqltagged template insrc/lib/db.ts. - No ORM, no migration tool. Tables are created and evolved by idempotent
ensure*Schema()functions (CREATE TABLE IF NOT EXISTS+ALTER TABLE … ADD COLUMN IF NOT EXISTS) defined insrc/lib/vendor-schema.ts,src/product/booking/schema.ts,src/product/master/schema.ts,src/lib/operators.ts,src/lib/influencers.ts,scripts/init-db.ts, and per-add-onschema.tsfiles. - The model covers the Tours domain (tours=
listings→ departures=units,bookings, operators=hosts, inclusions / itinerary master items, reviews) plus revenue-share (operator earnings, payouts, coupons, extras, influencer commissions) and a CMS (blogs, pages, FAQs, hero sliders, testimonials). See the Database Documentation.
Storage
- File uploads are sent to Cloudflare R2 (or any S3-compatible bucket) through the
AWS S3 SDK in
src/lib/r2.ts. - The active storage provider and credentials are read from the
integration_connectionstable (channelSTORAGE), encrypted with AES-256-GCM, withR2_*environment variables as a fallback. - Images can be transformed with
sharp(configurable thumbnail dimensions/format) before upload. Clients receive the public object URL.
Third-Party Services
| Provider | Used for |
|---|---|
| Neon | Managed serverless PostgreSQL (production) |
| Cloudflare R2 / S3-compatible | Object storage (tour media, images, attachments) |
| CreativeCape license server | Domain license activation + heartbeat (RS256, verified offline) |
| SMTP / email providers | Transactional email (verification, OTP, booking notifications) |
| Payment gateways | Booking checkout (COD, Bank, PayPal free; Mollie, Paystack, … as add-ons) |
| SMS / WhatsApp | OTP + notifications (via messaging add-ons) |
| Video hosts | Tour / marketing video (YouTube/Vimeo free; Bunny — add-on) |
| Analytics / pixels | GA4 (free); Clarity, GTM, Meta Pixel, PostHog, TikTok — add-ons |
| Captcha / live chat / chatbot | reCAPTCHA; Crisp/Intercom/Tawk/Tidio/Zendesk; OpenAI/Anthropic/OpenRouter |
Which providers are active is resolved at runtime from admin-configured
integration_connections, and provider code ships in the corresponding add-on — so
integrations activate without changing core code. Secrets are stored encrypted and never
returned to the client in plaintext.
© CreativeCape Solutions · creative-cape.com · support@creative-cape.com