This repository is the actively maintained production website and content-management application for Deep Dive Brewing Co., live at https://deepdivebrewing.com. This policy describes the current implementation; the code is the final source of truth, and docs/TECHNICAL.md is the detailed architecture reference.
If you discover a security vulnerability, please report it responsibly.
Do not open a public GitHub issue, pull request, or discussion for a suspected vulnerability. This is a public repository; a public report discloses the issue before it can be fixed.
Instead, email info@deepdivebrewing.com.
A useful report includes:
- A description of the vulnerability and its potential impact.
- The affected area — a URL, route, or component (for example
/api/admin/*,firestore.rules,storage.rules, or the trade-inquiry form). - Steps to reproduce, including the access level used (anonymous, signed-in non-admin, admin, superadmin).
- Relevant request/response details, console output, or error messages — with any credentials, tokens, or private keys redacted.
- Whether you were able to read or modify data you should not have been able to.
Never include real secret values in a report.
- We will acknowledge receipt and investigate the report.
- We may follow up for clarification or reproduction details.
- We will deploy a fix to production once confirmed and validated.
- Please allow a reasonable window for investigation and remediation before any public disclosure.
This policy covers:
- The Next.js application, including its API routes under
app/api/. - Firebase Authentication, Firestore, and Storage security configuration.
- The admin access model: custom claims,
adminUsers, invitations, and audit logs. - Environment-variable and secret handling in local development, CI, and Vercel.
- Third-party dependency vulnerabilities that affect this application.
- Authentication: Firebase Authentication, Google sign-in only. Signing in proves identity; by itself it grants no access.
- Authorization: custom claims on the Firebase ID token —
{ admin: true, role: "admin" | "superadmin" }— plus a matchingadminUsersrecord for the acting user that exists, hasstatus === "active", and carries the same role as the claims. Every privileged API route enforces this per request viarequireAdminActor/requireSuperAdminActorinlib/admin-auth.ts. Role agreement is required because embedded token claims lag server-side role changes until the token is refreshed — a demoted admin holding a stale token must not keep elevated access. Two lifecycle routes are deliberate exceptions:/api/admin/bootstrap(creates the first superadmin, gated bySUPER_ADMIN_EMAIL) and/api/admin/invitations/accept(creates the record on acceptance, gated by a pending invitation and verified email). - Roles:
adminmanages content and can trigger rebuilds;superadminadditionally manages administrators and invitations. Policy guards prevent demoting, disabling, or revoking the last active superadmin and the protectedSUPER_ADMIN_EMAILbootstrap account. - Invitations:
adminInvitationsrecords track pending/accepted/cancelled state. Superadmins create invitations through/api/admin/users; the invite email is sent via Resend; acceptance at/api/admin/invitations/acceptcreates theadminUsersrecord and sets custom claims transactionally, with rollback on partial failure. - Audit trail: administrative actions are appended to
adminAuditLogs. Rules allow superadmin create/read but deny all client updates and deletes, so the trail is immutable to clients. - Admin SDK boundary:
lib/firebase-admin.tsand the server-sidelib/admin-*.tsmodules areserver-only. The Admin SDK bypasses Firestore and Storage security rules, so it must never run in client code or be initialized from client-visible configuration. - Client SDK boundary: public reads of
isPublicbeer/venue documents, plus the admin dashboard's content writes (beers, venues,meta/siteRebuild, Storage uploads), are authorized byfirestore.rulesandstorage.rules. The rules enforce the same invariant as the API routes: valid claims plus an existing, activeadminUsersrecord whose role matches the claims — so a stale token cannot keep writing after an admin is disabled or demoted. Storage rules do this through a cross-servicefirestore.get()lookup against the(default)database. A catch-all rule denies everything else. - Protected admin APIs: every privileged
/api/admin/*operation requires a verified ID token, valid claims, and an activeadminUsersrecord for the acting user; administrator-mutation routes (users,users/[uid],invitations/[id]/resend) additionally require thesuperadminrole. (/api/admin/mealso exposes a non-privileged probe path that reports bootstrap eligibility or a pending invitation to signed-in users without claims.) - Rebuild authorization:
POST /api/admin/rebuildrequires the same token + claims + active-record check before calling the Vercel deploy hook. The hook URL is a server-only secret, and a per-instance cooldown (ADMIN_REBUILD_COOLDOWN_MS) limits trigger frequency. - Security headers:
next.config.tssets a Content-Security-Policy,X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, and HSTS, and enforces HTTPS + apex-domain 308 redirects. - UI is not authorization: the dashboard hides controls a user cannot use, but that is convenience only — enforcement lives in the API routes and the Firebase security rules.
See docs/TECHNICAL.md §6–§8 and §15 for the full description.
These values are embedded in the client bundle and are intentionally visible to browsers. They identify the Firebase and analytics projects; they are not authorization secrets and do not need rotation merely because they are publicly visible:
NEXT_PUBLIC_FIREBASE_API_KEY,NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,NEXT_PUBLIC_FIREBASE_PROJECT_ID,NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,NEXT_PUBLIC_FIREBASE_APP_ID,NEXT_PUBLIC_FIREBASE_MEASUREMENT_IDNEXT_PUBLIC_SITE_URLNEXT_PUBLIC_GTM_ID
True secrets and capability URLs. Disclosure of any of these warrants rotation — see docs/operations/credential-rotation.md:
FIREBASE_ADMIN_PRIVATE_KEY— service-account private key (PEM; stored with\nescapes). Grants full Firebase Admin access.RESEND_API_KEY— Resend API key. Authorizes sending email. Supplied by the Vercel-managed Resend integration in deployed environments.VERCEL_DEPLOY_HOOK_URL— capability URL: anyone holding it can trigger a production deploy.VERCEL_REBUILD_DEPLOY_HOOK_URL— legacy fallback deploy-hook variable, still supported by the rebuild route; treat identically.
Not cryptographic secrets, but they must stay out of the client bundle, documentation, tests, and logs:
FIREBASE_ADMIN_PROJECT_ID— Admin SDK project id (falls back toNEXT_PUBLIC_FIREBASE_PROJECT_ID).FIREBASE_ADMIN_CLIENT_EMAIL— service-account email paired with the private key.SUPER_ADMIN_EMAIL— the only account/api/admin/bootstrapwill promote and the email protected from demotion/disable/revocation.TRADE_INQUIRY_TO_EMAIL— trade-inquiry recipient inbox.RESEND_FROM_EMAIL— shared default sender (trade emails; fallback for invitations). Optional — senders resolve tonoreply@mail.deepdivebrewing.comwhen unset.ADMIN_INVITE_FROM_EMAIL— preferred invitation sender.ADMIN_REBUILD_COOLDOWN_MS— rebuild cooldown (default600000).ADMIN_INVITE_RESEND_COOLDOWN_MS— invitation resend cooldown (default60000).
Names and roles are also listed in .env.local.example (names only) and
docs/TECHNICAL.md §13. Values are never committed.
Never log:
- API keys (
RESEND_API_KEYor any provider key). - Firebase Admin credentials, especially
FIREBASE_ADMIN_PRIVATE_KEY. - Firebase ID tokens, session tokens, or
Authorizationheader values. - Deploy-hook URLs (
VERCEL_DEPLOY_HOOK_URL,VERCEL_REBUILD_DEPLOY_HOOK_URL). - Raw environment dumps (
process.env,env,printenv,.env.localcontents). - Customer or private data beyond what an operation genuinely needs (trade-inquiry contents, admin email addresses in unrelated logging).
The same rules apply to error telemetry sent to Sentry — lib/monitoring.ts
enforces them with sendDefaultPii: false, an event scrubber that strips
request headers/cookies/bodies/user context/query strings, and
sanitizeError, which replaces raw exceptions with a reduced representation
(sanitized name/message, scrubbed stack frames) since provider error text can
embed customer data or secrets. See
docs/operations/observability.md.
Accidental-disclosure surfaces to watch: build caches (.next/,
Turbopack/webpack caches), terminal output and scrollback, CI logs,
screenshots and screen recordings, and AI/agent transcripts. If a secret
reaches any of these, follow the incident checklist in
docs/operations/credential-rotation.md.
.env.localmust remain gitignored..gitignorecovers.env*and service-account JSON files; only.env.local.example(names and placeholders) is committed.- Restore a lost or stale
.env.localfrom authoritative sources —vercel env pull/ the Vercel dashboard, the Firebase console, and the Resend dashboard — per docs/operations/credential-rotation.md. - Do not treat
.next/caches, shell history, or tool output as secret stores; values recovered from them may be stale and should be treated as potentially exposed. - Never paste production credentials into committed example files, tests, or fixtures.
- CI (
.github/workflows/ci.yml) is intentionally secret-free: it runs typecheck, lint, tests, build, and the Markdown-link check withpermissions: contents: readand no secrets. - No environment variables are injected anywhere in CI. Service clients
initialize lazily (
getResendClient()inlib/resend.ts,getFirebase*()getters inlib/firebase.ts), sonext buildevaluates no service clients; Firestore reads during static generation resolve empty when no project config is present. Do not add real Firebase Admin, Resend, or Vercel secrets to CI merely to make it pass.
- The public
NEXT_PUBLIC_FIREBASE_*client config only identifies the project. Possessing it grants nothing;firestore.rulesandstorage.rulesare the authorization boundary for all client SDK traffic. - The Admin SDK bypasses security rules entirely, so its credentials and every module that uses them must remain server-only.
- Admin access requires both valid custom claims and an existing, active
adminUsersrecord whose role matches the claims; either alone is insufficient — on the protected API routes (lib/admin-auth.ts) and in the Firestore/Storage security rules alike. Each privileged request reads the acting user'sadminUsersdocument once (repeated lookups to the same document are cached within a single rules evaluation). - UI visibility (hidden buttons/tabs) is never an authorization boundary.
- CI gates every pull request to
main:tsc --noEmit, ESLint, thenode:testsuite, Firebase emulator rules tests, a production build, the Markdown-link check, and a check thatreact/react-domdeclare the same version (npm run check:react-versions). - Dependabot (
.github/dependabot.yml) opens weekly npm and GitHub Actions update PRs on Mondays, labeledtype: dependencies. Routine minor/patch tooling updates are grouped; production runtime packages and all major updates arrive individually for manual review. React and ReactDOM minor/patch updates are grouped when both have releases available — the CI version check, not Dependabot, enforces that their declared versions stay aligned. Auto-merge is not enabled — every update passes the same required CI as any other PR. - Dependabot security updates are a separate GitHub mechanism and may open PRs outside the weekly cadence; they are not disabled.
npm auditfindings are triaged routinely; not every advisory is fixable by Dependabot (transitive/dev-only chains may need targeted work).
Step-by-step procedures for rotating Resend, Firebase Admin, Vercel
deploy-hook, and bootstrap configuration — plus .env.local recovery and
accidental-disclosure response — live in
docs/operations/credential-rotation.md.