Skip to content

Architecture & Controls

This page is for IT leads, accountants, and security officers who need a technical overview. If you are an operator and want the short version, see the overview page.

CampOne is a multi-tenant SaaS application. Backend (Django 5.2, Python 3.12), admin frontend (React 19, Vite 6), and database (Supabase PostgreSQL, EU region) are three clearly separated components that communicate exclusively over encrypted connections (TLS 1.2+).

ElementValue
Token schemeJWT (RFC 7519), HS256, Bearer header
Access-token lifetime30 minutes, in-memory only in the browser
Refresh-token lifetime7 days, in an HttpOnly; Secure; SameSite=None cookie
RotationRefresh tokens are exchanged on every use; the prior token is immediately blacklisted
LogoutServer-side blacklist of the refresh token plus cookie deletion
Password policyMinimum length, similarity check against user attributes, common-password denylist, no purely numeric passwords
Password hashingDjango default pipeline (PBKDF2 primary, Argon2 available), cost factor rotatable
Password resetSingle-use HMAC token, default validity 3 days
Session limitPer-tenant cap by plan (e.g. Starter 2, Professional 5) — oldest sessions are revoked when exceeded

Every request passes through a TenantMiddleware that resolves the tenant in this order:

  1. From the tenant_id claim in the JWT (for authenticated calls).
  2. From the X-Tenant-Slug header (for unauthenticated public endpoints).
  3. From the custom domain in the Origin header (for guest-facing booking sites).

A TenantManager then automatically applies the filter tenant=<current> to every database query. Cross-tenant access requires explicit, grep-able tenant-switching code.

Inactive tenants (is_active=False) are never resolved — deactivation acts as an immediate kill switch.

RoleDescription
SuperadminCampOne-internal support. No tenant assigned. Real-tenant access requires an approved support request.
Tenant adminFull access within own tenant. Can create staff and assign modules.
StaffRestricted access to assigned modules (e.g. reception only or POS only).
CustomerGuest with access to their own bookings via the guest portal.

CampOne support can access a real tenant in one of three modes, every time recorded in the audit log:

  • Mock: a shared synthetic sample tenant. No approval required.
  • Redacted: your tenant, but PII is masked in API responses, and write operations are blocked server-side. No approval required.
  • Real: your tenant with full access. Requires an approved support-access request. Token lifetime 30 minutes.
HeaderValue
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload (1 year, preload list)
Content-Security-Policywith frame-ancestors 'none' and connect-src / img-src restricted to known origins
X-Frame-OptionsDENY
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), microphone=(), camera=()

All four shipped frontends (admin, marketing, docs, per-tenant booking SPAs) set these headers at the Vercel edge in addition to the backend configuration.

Data typeStorageProtection
DatabaseSupabase PostgreSQL, AWS Frankfurt region (eu-west-1)TLS, daily backups, point-in-time recovery
File uploadsSupabase S3, EU regionprivate ACL, tenant-scoped paths, signed URLs for every access
Tenant secrets (Stripe keys, SMTP passwords)DatabaseApplication-level encrypted fields, separate key per environment
LogsBackend host volumeRotation at 5 MB, max. 5 generations, no PII in query parameters
EndpointLimit
Login10 attempts/minute per IP
Password reset5 attempts/hour per IP
Account deletion3 attempts/hour per account
Public availability search60 requests/minute per IP
AI assistant (anonymous)20 requests/hour per IP
Public API per keyconfigurable, default 1000/hour

In addition, the application enforces a per-tenant concurrent-session cap — oldest refresh tokens are blacklisted when exceeded.

  • Public API (/api/v1/public/): API-key authentication with format ck_<prefix><secret>; only the salted hash is stored in the database, the cleartext is unrecoverable after creation. Every call is logged in APICallLog with status, path, latency. Bodies are not logged — the audit trail does not contain personal data.
  • Webhooks: Stripe webhooks are signature-verified using the per-tenant webhook secret. The OTA webhook (Booking.com) authenticates via HTTP Basic over TLS.
  • Lead form on the marketing site: validated client-side via react-hook-form and zod, server-side throttled at 20 requests/hour per IP.

Six separate, tenant-scoped, indexed tables record relevant events:

  • UserLoginLog — every successful login with IP and timestamp.
  • BookingAuditLog — every booking change as a diff entry with actor and reason.
  • SupportAuditEvent + SupportAccessSession — every CampOne support session into your tenant.
  • APICallLog — every public-API call.
  • MessageLog — every transactional email sent.
  • ErrorLog — every unhandled backend error.

Detailed description on the audit-trail page.

CampOne runs on the latest major versions:

ComponentVersion
Django5.2
Django REST Framework3.17
SimpleJWT5.4
React19
Vite6
Python3.12
PostgreSQL15 (Supabase)

There are no end-of-life components in production. Security patches roll into the next maintenance release; critical updates ship out of the regular release cycle.

This page is a summary. For a technical audit, on request we provide:

  • the internal security-posture document with file-level references for every control,
  • the nDSG compliance audit report,
  • the current state of the security roadmap with risk ratings,
  • access to a test environment for independent verification.

Requests to security@campone.ch.