Royal Glow internal docs · now fully interactive — Steps, API tables, file trees & live status
Royal Glow Docs

Security

How Royal Glow protects customer data — input validation, rate limiting, security headers, file uploads, webhooks, consent, and role-based access.

Security

In one line: Security is layered — Zod validation, Upstash rate limits, nonce-based CSP, exact-origin CORS, file-type whitelisting, HMAC webhook verification, DPDP consent, six-role RBAC, and hardened session cookies all work together so one gap never becomes a breach.

Royal Glow handles real people's personal details — names, phone numbers, dates of birth, booking history, and invoices. Keeping that data safe is not an afterthought; it is built into every layer of the platform. This page explains, in plain terms first and then in technical detail, the controls that protect the site and the people who use it.

Security is layered. No single control is relied upon on its own — validation, rate limits, headers, access checks, and consent all work together so that one gap does not become a breach.

The short version

  • Every piece of data sent to the server is checked before it is trusted.
  • Visitors cannot hammer the site with requests — each endpoint has a limit.
  • The browser is told exactly which scripts and origins are allowed.
  • Uploaded files are restricted to safe types and a sensible size.
  • Messages from outside services are verified before they are acted on.
  • People only see and do what their role allows.
  • We ask for consent before collecting analytics or marketing data, as required by India's Digital Personal Data Protection (DPDP) Act.

Input validation — Zod on every API input

Nothing from a client is trusted until it has been validated. Every API route parses its input through a Zod schema with safeParse() before any business logic runs. Schemas live in packages/types, shared between the client form and the server, so the contract is defined in exactly one place.

export const POST = withErrorHandler(async (req: Request) => {
  const body = await req.json()
  const parsed = createBookingSchema.safeParse(body)

  if (!parsed.success) {
    throw new AppError({
      code: 'VALIDATION_ERROR',
      message: 'Invalid request data',
      statusCode: 400,
      details: parsed.error.flatten().fieldErrors,
    })
  }

  // parsed.data is now fully typed and trusted
})

Use safeParse(), never parse(). parse() throws an unstructured error; safeParse() lets the route return a clean VALIDATION_ERROR with field-level details. Raw client input must never be used past the API boundary.

This also defends against mass assignment — extra fields a caller might try to sneak in are simply not part of the schema, so they are dropped.

Rate limiting — Upstash sliding windows

Every API endpoint is rate limited in Next.js middleware using @upstash/ratelimit backed by Upstash Redis. Limits use a sliding window keyed by ${ip}:${pathname}, and run before auth checks or business logic so abusive traffic is rejected as early as possible.

Limits are tuned per endpoint to the work they do:

EndpointMethodWindowLimitWhy
/api/servicesGET10s20Public catalog — generous for page loads
/api/availabilityGET10s10Prevents slot scraping
/api/leadsPOST1 min3Lead-form spam prevention
/api/auth/sign-in/*POST1 min10Brute-force protection
/api/bookingsPOST1 min5Normal customer booking rate
/api/webhooks/ablyPOST1s100Server-to-server burst tolerance

When a caller exceeds a window, the API returns 429 with the RATE_LIMITED code and retryable: true. Sensible fallbacks apply to any route not listed explicitly (for example, other POST /api/* routes default to 10 per minute).

Security headers — CSP with a nonce

The app sets a strict Content-Security-Policy so the browser only executes scripts the server explicitly trusts. Each response carries a per-request nonce, and inline scripts must present that nonce to run — this neutralises most cross-site scripting (XSS) attempts even if malicious markup slips through.

The nonce is generated fresh per request (alongside the request ID) and injected into both the CSP header and the allowed <script> tags. A static nonce would defeat the purpose.

CORS — exact-origin matching

Cross-origin requests are checked against an exact allowed origin — theroyalglow.in. Wildcards (*) are never used for credentialed endpoints. Anything from another origin is rejected rather than reflected back.

File uploads — type whitelist and size cap

Uploads (such as customer or content images and PDF documents) are constrained on two axes:

Prop

Type

Files are stored in Cloudflare R2 and served from a dedicated public CDN host, never executed by the application.

Webhook verification — HMAC signatures

Inbound webhooks come from external services (QStash, Brevo, Ably, Meta, AiSensy). Each is verified with an HMAC signature check before its payload is acted on, so a forged request cannot trigger real work.

// QStash callbacks are verified with the current + next signing keys
const receiver = new Receiver({
  currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY,
  nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY,
})

const isValid = await receiver.verify({ signature, body })
if (!isValid) {
  throw new AppError({ code: 'UNAUTHENTICATED', message: 'Invalid signature', statusCode: 401 })
}

Each provider has its own verifying secret — for example META_CAPI_WEBHOOK_TOKEN and AISENSY_WEBHOOK_SECRET. See Environment Variables for the full list.

Output safety — no unsanitised HTML

The codebase does not use dangerouslySetInnerHTML without sanitisation. This is enforced by the linter (noDangerouslySetInnerHtml is an error in the Biome + Ultracite config), so risky markup injection cannot reach production unnoticed.

Royal Glow operates under India's Digital Personal Data Protection (DPDP) Act. Data is stored on Indian servers and is not shared with third-party organisations. Consent is explicit and granular:

  • Required at onboarding — agreement to the Privacy Policy.
  • Optional — analytics — anonymous usage analytics (PostHog + Clarity) load only when the customer opts in.
  • Optional — marketing — offers and promotions (Meta Pixel + CAPI) fire only when the customer opts in.

Consent choices captured at onboarding seed the rgss_cookie_consent store, so the cookie banner does not re-ask for categories the customer has already decided on. See Authentication for the onboarding flow.

Access control — six RBAC roles

Authorisation uses Better Auth's roles plugin with six concrete roles in a strict hierarchy. Each role inherits the access of those below it.

customer < staff < receptionist < manager < owner < developer
RoleTypical access
customerPublic site, own bookings, profile, invoices
staffOwn schedule, assigned booking notes, leave requests
receptionistBookings, check-in, billing, memberships, leave approvals
managerStaff, services, reports, scheduling, settings
ownerFull business access, including admin.theroyalglow.in/users
developerEverything, plus integrations and log surfaces

A request that is authenticated but lacks the required role receives 403 with the FORBIDDEN code — distinct from 401 UNAUTHENTICATED, which means no valid session at all.

Session cookies — HttpOnly, Secure, SameSite=Lax

Sessions are managed by Better Auth and stored in Neon (PostgreSQL), not in a third-party service. The session cookie is hardened:

Prop

Type

Because Google OAuth runs on Royal Glow's own domain, the callback at theroyalglow.in/api/auth/callback/google keeps the session first-party. See Authentication for the full sign-in design.

Security checklist

Every new API route and feature is held to this list:

  • Zod validation on every input, via safeParse()
  • Per-endpoint @upstash/ratelimit sliding window applied in middleware
  • CSP header with a per-request nonce
  • CORS restricted to the exact theroyalglow.in origin
  • File uploads limited to jpg/png/webp/pdf, 10 MB max
  • HMAC signature verified on all inbound webhooks
  • No dangerouslySetInnerHTML without sanitisation
  • DPDP consent respected before analytics/marketing load
  • Role checked against the six-role hierarchy
  • Session cookies set HttpOnly, Secure, SameSite=Lax
OpenReport an issue

Was this page helpful?

On this page