Skip to main content

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.
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: 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: 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.
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.
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: 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
Last modified on June 29, 2026