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

MERMAID
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 the admin_token cookie.
  • Traveler portal (src/app/customer) and Operator portal (src/app/operator) — gated by the member_session cookie; the operator is resolved from a linked hosts row (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:

TEXT
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.

TEXT
┌──────────────────────── 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:

MERMAID
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 via postgres.js. Both are exposed through the same sql tagged template in src/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 in src/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-on schema.ts files.
  • 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_connections table (channel STORAGE), encrypted with AES-256-GCM, with R2_* 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