Closes out the design pass with the missing piece: a real client-side
mock-API pipeline so the write-path CTAs the design shows (invite a
teammate, run a scan, kick off a workflow test, request reactivation)
actually do something visible without a backend.
* `public/mockServiceWorker.js` — generated by `pnpm exec msw init`.
* `src/mocks/handlers.ts` — POST handlers for `/api/team/invites`,
`/api/scans`, `/api/workflows/:id/test`, `/api/billing/reactivate`.
Each returns the design's mono status-code header
(`201 · invite.created`, `202 · scan.queued`, etc.) so the toast
surface reads identical to the handoff. A `x-bp-tenant-status` hint
header lets the same handler respond 402 (frozen) or 410 (archived)
without needing a real session.
* `src/mocks/browser.ts` — thin `setupWorker(...handlers)` wrapper,
imported lazily so prod bundles don't pull MSW.
* `src/components/portal/MockWorker.tsx` — client component that boots
the worker only when `window.__BP_MOCK_API__` is true (set by
`[slug]/layout` when `BP_DEV_FIXTURE` is on the server). Real Auth.js
builds skip the worker entirely.
* `src/components/portal/ToastHost.tsx` — global bottom-right toast
queue, mounted in `[slug]/layout`. Emits via a custom event so any
client component can call `toast({ msg, code })` without prop-drilling.
* `src/components/portal/InviteButton.tsx` — first live write affordance.
Modal with email + role-segmented buttons, POSTs to `/api/team/invites`
with the tenant-status hint header, surfaces 201/402/410 differently.
Wired into the Team page.
* `src/middleware.ts` — added `mockServiceWorker.js` to the matcher
exclusion list so the host-rewrite doesn't 404 the worker script.
Verified end-to-end via Playwright: SW registers at the root scope,
click Invite member → fill email → Send invitation → MSW intercepts →
toast "Invitation sent · 201 · invite.created" → modal closes.
This closes the last open M10.2 task. Branch is ready to review/merge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
portal
Next.js 16 customer area + backstage.
Part of the Breakpilot Platform. For the big picture see
platform/docs: Architecture · Infrastructure · Product Integration Spec · Implementation Plan
What this is
Next.js 16 customer area + backstage. Scaffolded under milestone M5.1. See platform/docs for the full architecture context.
Plane: Control Owner: @sharang Status: pre-alpha Linked milestone: M5.1
Run locally
# Prerequisites: Node 20+, pnpm 9+, the dev stack running.
# 1. Bring up Keycloak + Postgres + Redis (separate clone):
cd /path/to/platform/orca-platform && make dev-up
# 2. Run tenant-registry (separate clone):
cd /path/to/platform/tenant-registry && make dev
# 3. Run this app:
make install # pnpm install --frozen-lockfile
make dev # next dev on http://localhost:3000
# Or hit a real tenant immediately:
# open http://acme.localhost:3000 → redirects to Keycloak → back to /acme/dashboard
Seed login (from the dev-stack realm): test@breakpilot.dev / test.
AUTH_URL gotcha: Auth.js v5 builds the OAuth
redirect_urifromAUTH_URL— not from the request Host header, even withAUTH_TRUST_HOST=true. For multi-tenant dev work, pinAUTH_URLto the subdomain you log in on (e.g.,http://acme.localhost:3000); otherwise Keycloak rejects the token exchange withinvalid_grant: Incorrect redirect_uri. In prod, orca-proxy passes the right host viaX-Forwarded-HostandAUTH_URLis set to the apex (https://breakpilot.com).
make test / make lint / make typecheck / make build run vitest / eslint / tsc / next build respectively.
Env vars live in .env.example. Copy to .env.local for local overrides (gitignored).
Surface
| Route | Renders |
|---|---|
http://localhost:3000/ |
Apex landing — pointer to tenant subdomains |
http://<slug>.localhost:3000/ |
Middleware rewrites to /[slug]/ → redirects to /[slug]/dashboard |
http://<slug>.localhost:3000/dashboard |
OIDC-gated dashboard; signed-out users see "Sign in with Keycloak" |
http://backstage.localhost:3000/ |
(Skeleton) backstage route — rewritten to /__backstage__/* |
/api/auth/[...nextauth] |
Auth.js v5 endpoints (callback, signin, signout, jwt) |
Architecture notes
- Host → slug routing:
src/middleware.tsparsesHostheader viaparseHost()(insrc/lib/host.ts) and rewrites the request path to/<slug>/.... URL bar stays unchanged. Apex hosts and unknown subdomains fall through unmodified. - Tenant context:
src/app/[slug]/layout.tsxfetches the tenant fromtenant-registry(src/lib/tenant-registry.ts). 404 →notFound(); HTTP errors bubble up. - Auth:
src/auth.tsis the Auth.js v5 config — Keycloak provider, tenant-context claims (tenant_id,tenant_slug,org_roles,products,plan,tenant_status) propagated via JWT/session callbacks. Real RBAC enforcement lands in M5.2 / M10.1.
Deployment
| Env | URL | How |
|---|---|---|
| dev | http://localhost:3000 |
make dev |
| stage | https://portal.stage.breakpilot.com |
auto on merge to main |
| prod | https://portal.breakpilot.com |
manual: tag vX.Y.Z + sign-off |
Rollback: orca rollout undo portal --env={{env}}.
Observability
- Traces, logs, metrics: SigNoz — service name
portal - Audit events: Tenant Registry
/audit(Retraced-shape schema) - On-call:
oncall@breakpilot.com· runbook atplatform/docs/runbooks/portal.md
Contributing
See CONTRIBUTING.md. TL;DR: branch from main, open a PR, 1 review + green CI, squash-merge.
End-to-end tests (M5.3)
Playwright config at playwright.config.ts. Tests under tests/e2e/.
make e2e-install # one-time: pnpm exec playwright install chromium
# bring up the dev stack + tenant-registry + portal in three separate terminals,
# then:
make e2e # pnpm playwright test
Test groups (filter with --grep):
| File | What it asserts |
|---|---|
tests/e2e/apex.spec.ts |
Apex landing page renders |
tests/e2e/tenant.spec.ts |
Tenant subdomain serves signed-out dashboard + 404 on unknown slug |
tests/e2e/health.spec.ts |
The whole dev stack is reachable: portal API, tenant-registry, Keycloak |
@needs-stack in a test title means the dev stack must be running. We don't yet have a full OIDC click-through test — Keycloak in headless mode is flaky, so we assert the gate (Sign-in button visible) rather than completing the login.
In CI, the e2e job is gated behind the repo variable RUN_E2E == 'true' so it stays off until stage exists. Lint / typecheck / build / vitest still run on every PR.
License
Proprietary — all rights reserved. Copyright (c) 2026 Sharang Parnerkar and Benjamin Boenisch. See LICENSE.