Blackmass corporate site + indaba.zimx.io internal ops portal, built on Next.js 14 and Supabase.
npm install
cp .env.local.example .env.local # then fill in the required env vars
npm run devThe app runs at http://localhost:3000. Marketing pages live at /, /about,
/contact, /press. The ops portal lives under /indaba and is served on
indaba.zimx.io in production via hostname-based routing (see
middleware.ts).
See .env.local.example for the full list.
NEXT_PUBLIC_SUPABASE_URL— project URL; safe to ship to the browserNEXT_PUBLIC_SUPABASE_ANON_KEY— publishable anon key (RLS-scoped)SUPABASE_SERVICE_ROLE_KEY— bypasses RLS; server-only, never commit
HEALTH_CHECK_TOKEN— any opaque random string.GET /api/ops/healthreturns 403 unless the request carriesX-Health-Token: <value>.
Auto-deploys to Vercel on push to the default branch. blackmass.co.uk (and
the Vercel preview URL) serve the marketing site; indaba.zimx.io serves the
internal ops portal off the same deployment.
These do NOT live in code and must be completed in the Supabase dashboard / GitHub settings before the hardening pass is considered deployed:
- Supabase → Storage →
photos— flip the bucket from public to private. The RLS policies insupabase/migrations/20260421090100_storage_policies.sqlassume a private bucket; leaving it public leaves the legacy public-read path open in parallel. - Supabase → Settings → Auth → JWT expiry — set to
3600(1 hour). The client uses the refresh-token flow; shorter JWTs limit the blast radius of a leaked access token. - Supabase → Settings → API → Service role key — verify no historical
git commit contained the real key (checked 2026-04, clean). If it ever
does leak, rotate here and redeploy; note the rotation date in
CHANGES.md. - GitHub → Settings → Code security and analysis — enable Secret
scanning and Push protection on
roy-bme/blackmasswebsite. - Vercel → Environment Variables — set
HEALTH_CHECK_TOKENto a fresh random value and wire it into the uptime monitor. npm update— run locally after cloning to pull Next 14.2.35+ intonode_modules; commit the refreshedpackage-lock.jsonif your environment differs.- Basemap provider — we still load Carto tiles with lat/lng in the URL. Acceptable given internal-only usage; follow-up item to host tiles ourselves if we expand to customer-facing map surfaces.
Hostname-routed internal portal, Supabase auth via magic link, role-based
access with four roles (admin, ops, bd, compliance) enforced
server-side in lib/ops/auth.ts + lib/ops/nav.ts. All mutations go
through /api/ops/* Route Handlers which re-check role, call the
service-role key, and set attribution columns from the server session —
RLS (under supabase/migrations/) is defence-in-depth.
Modules:
- Dashboard — live KPIs
- Map — interactive Bulawayo map with business pins, supply links, zone overlays
- Directory — business directory (kanban + list views)
- Supply chain — graph + loop detection
- Introductions — BD pipeline with Roy approval gate
- Events — trade fairs, chambers, networking + post-event debriefs
- Ops feed — channel-based daily reporting (ground ops / BD / admin)
- ZITF — paper + digital stand-response pipeline
Healthcheck: GET /api/ops/health with X-Health-Token: $HEALTH_CHECK_TOKEN.
npm run dev— Next.js dev servernpm run build— production buildnpm run lint— ESLint + Next.js rulesnpm run typecheck—tsc --noEmitnpm test— unit tests (Node built-in runner, currently CSV escape)npm run audit:check— waiver-awarenpm audit --omit=devwrapper; fails on un-waived highs / criticals