Software Requirement Specification (SRS)
This Software Requirement Specification describes the technical requirements, interfaces, and quality attributes of Tours — a production-ready, self-hosted, white-label tours & experiences booking marketplace. It is intended for developers and technical buyers who will deploy, operate, or extend the system. It builds on the Business Requirement Document (business intent) and the Functional Requirement Document (functional behavior) to define how the software must be built and constrained.
| Field | Value |
|---|---|
| Product | Tours — tours & experiences booking marketplace |
| Vendor | CreativeCape Solutions |
| Document type | Software Requirement Specification (SRS) |
| Audience | Developers, technical buyers, integrators |
| Parent | BRD, FRD |
1. Introduction
1.1 Purpose
Tours is a single self-hosted Next.js 16 (App Router) application that serves several experiences from one codebase:
| Surface | Route group | Role |
|---|---|---|
| Storefront | (site) → / |
Public tour catalog, checkout, marketing |
| Customer portal | /customer |
Bookings, payments, and account |
| Operator portal | /operator |
Tour authoring, departures, earnings |
| Influencer portal | /influencer |
Referral links, commissions, payouts |
| Admin / CMS | /admin |
Operations, content, settings, licensing |
The application exposes a versioned REST API under /api/v1, an install API under /api/install, and a license API under /api/license. Data is persisted in PostgreSQL accessed with a parameterized tagged sql template (no ORM) via the @neondatabase/serverless driver. Media is stored in Cloudflare R2 (S3-compatible).
1.2 Scope
This SRS covers the runtime software: its stack, functional and non-functional requirements, external interfaces (REST API, storage, license server, third-party integrations), data model conventions, security model, and deployment constraints. Business justification lives in the BRD; user-facing behavior lives in the FRD.
1.3 Definitions
| Term | Meaning |
|---|---|
| Tour | A bookable experience (a listings row in the database). |
| Operator | A seller/host (a hosts row). |
| Departure | A dated, priced, capacity-limited option of a tour (a units row) that a booking is made against. |
| Traveler / Customer | The buyer who places a booking. |
| Inclusion / Extra | What a tour includes; an extra is an optional priced add-on. |
| Entitlement | A licensed capability unlocking a premium add-on (channel provider or feature). |
| Add-on | A packaged premium provider or feature activated by a purchase code / entitlement. |
| R2 | Cloudflare R2, S3-compatible object storage. |
| RBAC | Role-Based Access Control (admin resource:action matrix; member accountType). |
Domain ↔ DB note: product vocabulary is used in code and UI (tour, operator, departure); the underlying tables keep their original names (
listings,hosts,units, with columns likehost_id/listing_id/unit_id). Real table names appear only in SQL.
2. Technology Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) + React 19, TypeScript |
| Styling | Tailwind CSS, tailwind-merge, clsx |
| Database | PostgreSQL via @neondatabase/serverless — parameterized sql tagged template, no ORM |
| Auth | Custom JWT with jose and jsonwebtoken; bcryptjs password hashing |
| Secrets | Node crypto — AES-256-GCM encryption (src/lib/secrets.ts) |
| Storage | Cloudflare R2 / S3-compatible via @aws-sdk/client-s3 + presigner |
| Images | sharp (resize / crop / WebP–JPG conversion) |
nodemailer (SMTP) |
|
| Licensing | RS256 JWT via jsonwebtoken, verified offline against a bundled public key |
| UI / content | lucide-react, react-icons, framer-motion, marked / react-markdown, jspdf, jszip, leaflet |
The database client is lazily initialized (src/lib/db.ts) so next build does not require DATABASE_URL; queries use the tagged-template sql`SELECT …` form, with rawSql(query, params) for $1-style array queries. Idempotent ensure*Schema() functions provision tables on first use.
2.1 Brand & theme tokens
The UI is white-label: branding, content, currency, and locale come from settings — nothing is hardcoded. Styling uses design tokens only — ink-* for text/borders/surfaces and brand-* for the primary accent. The primary brand color is blue:
:root {
--brand-500: 59 130 246; /* blue-500 */
--brand-600: 37 99 235; /* blue-600 */
--brand-700: 29 78 216; /* blue-700 */
}
3. Functional Requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-1 | The system shall authenticate members and admins via signed JWTs in HttpOnly cookies (member_session 30d; admin_token 7d default). |
Must |
| FR-2 | The system shall enforce admin authorization through a resource:action permission matrix (requirePermission), and member authorization through accountType portal routing. |
Must |
| FR-3 | The system shall re-price departures, re-validate coupons and extras, and compute tax server-side at booking time. | Must |
| FR-4 | The system shall persist all domain data in PostgreSQL using the parameterized sql tagged template — no ORM. |
Must |
| FR-5 | The system shall upload media server-side to R2/S3 and process images with sharp before storage. | Must |
| FR-6 | The system shall confirm bookings automatically when payment reaches paid, and require an admin Mark as Paid action for offline bookings. |
Must |
| FR-7 | The system shall track departure availability and prevent booking of sold-out or past departures. | Must |
| FR-8 | The system shall gate premium add-ons behind license entitlements / purchase codes, unlocking dev/localhost hosts automatically. | Should |
| FR-9 | The system shall provide an install wizard (/install) that initializes the schema, optional demo data, license, admin user, and site settings. |
Must |
| FR-10 | The system shall store integration secrets AES-256-GCM encrypted and never return them in plaintext to clients. | Must |
| FR-11 | The system shall expose a versioned REST API (/api/v1) that mirrors every user-facing storefront/portal surface for mobile clients. |
Should |
4. Non-Functional Requirements
| ID | Attribute | Requirement |
|---|---|---|
| NFR-1 | Performance | Hot public reads (catalog, tour detail, destinations, facets) are cached server-side; serverless Postgres scales reads. |
| NFR-2 | Scalability | Stateless request handlers on serverless/edge-style hosting; no node-local session state. |
| NFR-3 | Reliability | License verification is offline; a grace window keeps a site live if the license server is briefly unreachable. |
| NFR-4 | Security | AES-256-GCM secret encryption, JWT auth, RBAC, bcrypt password hashing, server-side validation. |
| NFR-5 | Maintainability | Conventional Next.js App Router layout; typed TypeScript; core/product layer split for reuse. |
| NFR-6 | Portability | Any S3-compatible storage (R2, DigitalOcean Spaces, AWS S3, GCS) and any PostgreSQL with a serverless-compatible URL. |
| NFR-7 | Brandability | Theme, logo, copy, currency, and locale are admin-configurable; portals render dynamically to reflect live theming. |
| NFR-8 | Accessibility & responsiveness | Layouts are responsive (mobile-first, gated breakpoints); tables scroll on small screens; modals become bottom-sheets. |
| NFR-9 | Compliance | Secrets and password hashes are never serialized to clients. |
| NFR-10 | Internationalization | User-facing strings pass through t(); currency/locale come from settings, not code. |
5. External Interfaces
5.1 REST API (/api/v1)
A versioned REST API serves the storefront and portals. Conventions:
- Protected routes require the appropriate session cookie (or a Bearer token for mobile clients); admin routes additionally pass the permission matrix.
- Every route guards auth first and scopes queries to the owner (
AND customer_id = me/owner_id = operatorId) — no IDOR. - List endpoints accept pagination/filter params and return
{ data, meta: { total, page, per_page, total_pages, sort } }; detail endpoints return{ data }; money travels in minor units (cents). - Responses use the
ok(data)/err(msg, status)/serverErr(e)helpers.
Representative surfaces: tours, destinations, inclusions, extras, bookings (+ checkout, pay, verify, payment-methods), coupons, reviews, operator, influencer, customer, portal, blogs, pages, forms, i18n, plugins, cron.
// Route handler shape (src/app/api/v1/**/route.ts)
export async function POST(req: NextRequest) {
try {
const me = await getCustomerId();
if (!me) return err("unauthorized", 401); // 1) AUTH FIRST
const b = await req.json();
const name = String(b.name ?? "").trim().slice(0, 160); // 2) VALIDATE + COERCE
if (!name) return err("A name is required.");
const [row] = await sql` // 3) SCOPE TO OWNER
UPDATE listings SET name = ${name}
WHERE id = ${Number(b.id)} AND owner_id = ${me} RETURNING *`;
if (!row) return err("Not found", 404);
return ok(row);
} catch (e) { return serverErr(e); }
}
5.2 Storage Interface (Cloudflare R2 / S3)
All file operations go through an S3-compatible abstraction (src/lib/r2.ts). The active provider and credentials are read from integration_connections (channel STORAGE, primary first), with env fallbacks (R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_DEFAULT_BUCKET, R2_PUBLIC_URL). Images are processed with sharp into preset sizes (avatars, tour thumbnails/covers, blog, logo, etc.) and served from the storage CDN — not the application server.
5.3 License Server (creative-cape.com)
The product embeds the CreativeCape license client (src/lib/license/). It activates once (POST /api/license/activate) to bind a purchase code to the domain and receive an RS256 JWT, then verifies that token offline with a bundled public key on every check, re-validating on a daily heartbeat. Per-add-on purchase codes follow the same offline-verification model (src/lib/license/addons.ts), stored in addon_licenses and bound to { addon, domain }.
5.4 Third-Party Integration Interfaces
Integrations are admin-configurable and credential-supplied by the buyer; secrets are encrypted at rest.
| Category | Free default | Premium examples |
|---|---|---|
| Payments | Online gateway, Cash, Bank Transfer | Additional card/wallet gateways |
| SMTP (nodemailer) | SendGrid, Postmark, AWS SES, Mailgun | |
| Storage | Cloudflare R2 | DigitalOcean Spaces, AWS S3, Google Cloud Storage |
| Analytics | GA4 | GTM, Meta Pixel, PostHog, TikTok, Clarity |
| Video | YouTube, Vimeo | Mux, Cloudflare Stream, Bunny |
| Messaging | — | SMS (Twilio/Msg91), WhatsApp, live chat, AI chatbot |
| Auth / Security | — | Social sign-in (Google/Facebook), reCAPTCHA v2/v3 |
6. Data Model Conventions
- Schema is provisioned in-app by idempotent
ensure*Schema()functions (CREATE TABLE IF NOT EXISTS,ALTER TABLE ... ADD COLUMN IF NOT EXISTS) — no migration files. - Core entities: a tour is a
listingsrow, an operator is ahostsrow, a departure is aunitsrow; bookings, payments, payouts, coupons, reviews, and commissions reference them by id. - Money is stored as integer minor units (cents); dates as ISO/
DATE. - Object-level authorization is enforced with owner-scoped predicates; opaque
public_idvalues are preferred over serial ids in URLs.
7. Security
- Authentication. Stateless JWTs in HttpOnly, SameSite=Lax cookies —
member_session(HS256, 30d) for travelers/operators andadmin_token(HS256, 7d default) for admins. There is no refresh-token endpoint; clients re-authenticate on expiry. Passwords are hashed with bcryptjs. - Authorization. Members are routed by
accountType; admins pass a resource:action permission matrix (permissions-catalog.ts+requirePermission), with system/super roles bypassing the matrix and mutations emitting audit events. - Secret encryption.
src/lib/secrets.tsencrypts integration credentials with AES-256-GCM (12-byte IV + auth tag,enc:v1:prefix); the key derives fromSECRET_KEY/JWT_SECRET. Stored secrets are decrypted only server-side and never returned to clients. - Input handling. Departure pricing, coupons, extras, and tax are recomputed server-side; all client input is re-validated and length/bound-checked; uploads pass through the server with sharp processing before storage. SQL is always parameterized.
- Rate limiting. Auth and public write endpoints are rate-limited via
@/lib/rate-limit. - Transport. HTTPS is expected at the edge; tokens travel as cookies/bearer credentials, never in URLs.
8. Performance & Reliability
- Caching. Hot public reads (published tours, tour detail, destinations, operators, facets) are cached server-side with short TTLs; the license record is cached ~60s for hot public pages.
- Serverless data. PostgreSQL via the @neondatabase/serverless driver scales reads; the lazy client avoids build-time DB requirements.
- License fail-open grace. Offline RS256 verification plus a grace window means a brief license-server outage never takes a live site down.
- Media offload. Images convert and serve from R2/S3, offloading bandwidth from application nodes.
- Loading states. Every data-fetching page renders a layout-shaped skeleton while loading, preventing layout shift.
9. Constraints
- Serverless / read-only filesystem. The application targets serverless hosting where the filesystem is effectively read-only at runtime; all user media must go to R2/S3 rather than local disk.
- Buyer-supplied services. SMTP/email, SMS, WhatsApp, and payment-gateway credentials are provided by the buyer and configured in admin settings.
- Premium gating. Premium channel providers and features require a valid CreativeCape entitlement or per-add-on purchase code; development hosts (localhost/LAN/
*.test) are unlocked. - Database compatibility. Requires a PostgreSQL database reachable via a serverless-compatible connection URL (
DATABASE_URL). Use the pooled endpoint for the app and the direct endpoint for dumps/restores.
10. Acceptance Criteria (system level)
| ID | Criterion |
|---|---|
| AC-1 | A fresh install via /install yields a working storefront and admin without manual database setup. |
| AC-2 | A booking created online reaches paid and is confirmed automatically; capacity decrements and earnings/commission are recorded. |
| AC-3 | An offline booking stays pending until admin Mark as Paid, then confirms with the same side effects. |
| AC-4 | Attempting to book a sold-out or past departure is rejected server-side. |
| AC-5 | Integration secrets round-trip through the DB encrypted and are never returned in an API response. |
| AC-6 | An unlicensed premium provider is blocked server-side and hidden client-side on production hosts. |
| AC-7 | npx tsc --noEmit -p tsconfig.json passes; API responses conform to the { data, meta } / { data } shapes. |
For technical support, license activation, and extension guidance, contact creative-cape.com.
© CreativeCape Solutions · creative-cape.com · support@creative-cape.com