Identity-model rollout T2.4. Trusted view now derives `lookup_email = bb_mailbox or email` and uses it for both User.objects.filter() and the new-User row's email field. WARN-log fallback to federation email when the claim is absent (transitional safety; should never fire once Zitadel `bb-claims` Action + bridge-side userinfo enrichment are live). Decode-time required-claims unchanged (`bb_mailbox` stays optional) so partial deploys aren't bricked. Pre-migration SQL rename of operator's existing User row required — see binarybeachio docs/services/plane/migration-plan.md §9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
bb-plane-fork — binarybeachio customizations of Plane
This file is the canonical contract between this fork and the binarybeachio platform repo. It exists so anyone (or any agent) on a fresh session can answer "what's customized, why, and how do I refresh from upstream" without reading code.
Fork repo convention (template — same shape for every Path B fork in binarybeachio):
upstream remote → original project on github.com (read-only, merge-source)
origin remote → git.binarybeach.io/binarybeach/bb-<name>-fork (where we push)
github mirror → github.com/binarybeachllc/bb-<name>-fork (push-mirror, off-site backup)
upstream branch — clean mirror of upstream's default branch, never modified
main branch — our customizations on top of latest upstream tag we've integrated
update/<v> — short-lived integration branch when pulling a new upstream version
git log main..upstream = "upstream changes I haven't pulled in"
git log upstream..main = "binarybeachio's customizations"
Upstream
| Field | Value |
|---|---|
| Project | Plane (open-source project management) |
| Upstream repo | https://github.com/makeplane/plane |
| Upstream default branch | preview |
| Currently integrated upstream version | v1.3.0 (release commit cf696d2) |
| License | AGPL-3.0-only (we MUST publish source of any deployed customizations — public Forgejo + push-mirror to GitHub satisfies this) |
Why we forked (post-2026-05-04 platform-architecture pivot)
Plane's first-party OIDC support is gated behind the Pro/Business commercial edition (Pro tier minimum 25 users = $338+/mo). The community edition's /god-mode/authentication/oidc page is a frontend stub — the backend handler returns 404. Plane CE has GitHub/GitLab/Gitea/Google OAuth providers but no native OIDC, no SAML, and no trusted-proxy-header auth.
We integrate Plane into the binarybeachio platform via the architecture's Bucket 4 pattern: a single additive trusted-JWT endpoint that the platform's auth-bridge calls after oauth2-proxy validates a Zitadel session at the edge. See:
binarybeachio/docs/architecture/01-platform-architecture.mdfor the bucket taxonomy and bridge contractbinarybeachio/docs/architecture/bridge-jwt-replay-protection.mdfor the JWT replay-protection contractbinarybeachio/docs/services/plane/migration-plan.mdfor the full per-service migration write-up
The previous shape of this fork (in-place patching of provider/oauth/github.py to repurpose GitHub OAuth as Zitadel OIDC) was reverted on 2026-05-04 in favor of the Bucket-4 additive endpoint, which has a smaller fork surface, fewer hot files to track on upstream merges, and centralizes OIDC handling in the auth-bridge instead of duplicating it per-app. The pre-revert source state is preserved on the pre-migration-2026-05-04 branch for reference.
What's customized (the inventory — keep current)
Three logical patch groups across the repo. Touch surface is intentionally minimal.
Patch 1: Bucket-4 trusted-JWT entry-point (additive — 1 new file + 2 line additions)
| File | Change | Risk on upgrade |
|---|---|---|
apps/api/plane/authentication/views/app/trusted.py |
New file. Django View that validates a bridge-issued RS256 JWT, atomically claims its jti in shared-redis (replay protection), find-or-creates the User keyed on bb_mailbox (four-layer identity model — falls back to email when the claim is absent), and calls user_login(request, user, is_app=True) to set the Django session cookie. PEM is fetched at runtime from BB_BRIDGE_PUBLIC_KEY_URL (avoids the env-PEM corruption issue Coolify has with backslash-escaped keys). Endpoint is implicitly disabled (returns 404) when the env is unset. |
Low. Depends only on User model, user_login, post_user_auth_workflow, and get_safe_redirect_url — all stable upstream APIs. PyJWT and requests are existing deps. |
apps/api/plane/authentication/urls.py |
1-line addition appending path("sign-in-trusted/", TrustedSignInEndpoint.as_view(), name="sign-in-trusted") to the urlpatterns list. |
Low. Pure append; no existing routes modified. |
apps/api/plane/authentication/views/__init__.py |
1-line addition exporting TrustedSignInEndpoint. |
Low. Pure append. |
apps/api/plane/authentication/adapter/error.py |
Adds 7 error codes in the 6000–6099 range (reserved for fork additions). Pure dict-additions; no existing entries renumbered. | None. |
The full bridge ↔ Plane contract:
- Bridge mints
RS256JWT signed withBRIDGE_SIGNING_KEY(private). Claims:iss=bb-bridge,aud=plane,iat,exp(now+60s),jti(UUIDv4),sub,email,first_name,last_name,tenant, andbb_mailbox(when emitted by Zitadel'sbb-claimsAction — seebinarybeachio/docs/architecture/multi-tenant-identity.md§4). - Bridge 302s the user's browser to
https://pm.<tenant>.binarybeach.io/auth/sign-in-trusted/?token=<jwt>&next_path=<rd>. - Plane's view: fetches public key from
BB_BRIDGE_PUBLIC_KEY_URL(cached 5 min), verifies signature + claims, atomicallySETNX bb_bridge_jti:<jti>in shared-redis with TTL =exp - now + 30s, find-or-creates User keyed onbb_mailbox(preferred) oremail(fallback), callsuser_login(), 302s tonext_path. - Replay protection is fail closed: if shared-redis is unavailable, the request is rejected. Operator break-glass uses the email+password sign-in (vanilla upstream code) which doesn't depend on either Redis or the bridge.
Patch 2: Presigned PUT for uploads (R2/B2 don't implement PostObject)
| File | Change | Risk on upgrade |
|---|---|---|
apps/api/plane/settings/storage.py |
S3Storage.generate_presigned_post(...) rewritten to mint a presigned PUT URL via generate_presigned_url(HttpMethod="PUT"). Method name preserved for caller compat. |
Medium. If Plane's upload flow changes upstream, conflict surface grows. Candidate for upstream PR. |
apps/api/plane/utils/openapi/responses.py |
OpenAPI example response updated to PUT shape. | Low. |
apps/api/plane/tests/unit/settings/test_storage.py |
2 tests retargeted to assert generate_presigned_url boto3 call. |
Low. |
packages/types/src/file.ts |
TFileSignedURLResponse.upload_data adds method?: "PUT" | "POST", drops AWS POST-form-data fields. |
Low. |
packages/services/src/file/helper.ts |
generateFileUploadPayload(...) returns a TFileUploadRequest descriptor; dispatches PUT/POST. |
Medium. |
packages/services/src/file/file-upload.service.ts + apps/web/core/services/file-upload.service.ts |
uploadFile(...) signature changed to (payload, progress?). Uses axios.request({method, url, data, headers}). |
Medium. |
apps/web/core/services/file.service.ts, apps/web/core/services/issue/issue_attachment.service.ts, packages/services/src/file/sites-file.service.ts |
5 caller sites updated to pass TFileUploadRequest to uploadFile. |
Low. |
Decision record at binarybeachio/docs/features/storage-upload-flow.md. Patch 2 is independent of Patch 1 — git revert <storage-PUT sha> undoes it cleanly.
Patch 3: Brand asset (kept as dormant; entry-point UX is Traefik-driven)
| File | Change | Risk on upgrade |
|---|---|---|
apps/web/app/assets/logos/binarybeach-logo.png |
New asset. Currently unreferenced; preserved for future AGPL §13 footer-link addition or other branding work. | None. |
The previous fork's GitHub-button rebrand patch (apps/web/core/hooks/oauth/core.tsx) was reverted on 2026-05-04. Sign-in entry-point UX is now driven by a Traefik redirectregex middleware applied to the per-tenant Plane router that 302s /sign-in*, /sign-up*, /accounts/sign-in* to https://bridge.binarybeach.io/handoff?app=plane&tenant=<slug>&.... Pure infrastructure config; no source modification needed for the redirect.
Files not changed (deliberately):
apps/api/plane/authentication/provider/oauth/github.py— upstream-clean. Vanilla GitHub OAuth still works if configured via god-mode UI.apps/api/plane/authentication/views/app/github.pyand the gitlab/gitea/google equivalents — all upstream-clean.apps/admin/...— god-mode UI unchanged.apps/space/...— public-share OAuth unchanged. Authenticated public boards continue to use email+password sign-in (vanilla upstream). When a tenant needs SSO for shared boards, add a siblingviews/space/trusted.py(estimated ~80 LOC, mirrors the app/ view).
Required runtime config
Set on the patched plane-backend container (binarybeachio sets these in infrastructure/plane/.env):
# Activates the trusted-JWT endpoint. URL points at the in-cluster bridge
# service's public-key endpoint. Unset → endpoint returns 404 (regression-safe).
BB_BRIDGE_PUBLIC_KEY_URL=http://auth-bridge-<uuid>:3000/.well-known/bb-bridge.pub.pem
Bridge-side configuration (in binarybeachio/infrastructure/auth-bridge/.env):
ADAPTER_PLANE_BINARYBEACH_BASE_URL=https://pm.binarybeach.binarybeach.io
# BRIDGE_SIGNING_KEY is loaded centrally by bridge-key.ts; the matching
# public key is served at /.well-known/bb-bridge.pub.pem and consumed by
# Plane via BB_BRIDGE_PUBLIC_KEY_URL.
Plane god-mode admin UI (/god-mode/authentication/...):
- All four upstream OAuth providers (GitHub/GitLab/Gitea/Google) can be left disabled. The trusted-JWT entry-point is the SSO front door.
- Email+password sign-in remains available as the break-glass admin path. Per-tenant
bb-adminuser is seeded with a permanent password from_shared/.env.bb-admin.
Cross-fork conventions adopted
This fork pulls in binarybeachio's session lifecycle convention — 15-min idle timeout, slide-on-activity. Applied automatically by bootstrap.py at deploy. To override for this fork specifically, set SESSION_COOKIE_AGE / ADMIN_SESSION_COOKIE_AGE / SESSION_SAVE_EVERY_REQUEST in infrastructure/plane/.env (per-app .env beats convention).
Refresh from upstream — the procedure
When a new Plane release lands and we want to integrate:
git fetch upstream
# Sync the upstream mirror branch (never touched by us)
git switch upstream
git reset --hard upstream/preview # or @v1.4.0 if we track tags
git push origin upstream
# Integration branch
git switch main
git switch -c update/v1.4.0
git merge upstream # likely conflict-free since fork is additive
# Hand-test:
# 1. Local stack via docker-compose.bb-local.yml — confirm sign-in works.
# 2. Trusted endpoint with a hand-minted JWT (helper script TBD; for now,
# mint via a node REPL using bridge-key.ts:signBridgeJwt).
# 3. Vanilla email+password regression test.
# Once happy:
git switch main
git merge --ff-only update/v1.4.0
git branch -d update/v1.4.0
git push origin main
# Then on laptop: rebuild + tag + push images (see "Build" below)
# Then in binarybeachio repo: bump tag in infrastructure/plane/docker-compose.yml
# Then: py infrastructure/_shared/bootstrap.py to trigger the Coolify deploy
Build — which images to rebuild and how
Per binarybeachio architecture doc §7.4 ("only rebuild what we touched"), this fork only requires rebuilding two of the six Plane images:
| Image | Customized? | Source | Build target |
|---|---|---|---|
plane-backend |
YES (Patch 1 + Patch 2) | apps/api/Dockerfile.api |
git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.<n> |
plane-frontend (aka web) |
YES (Patch 2 frontend bits only) | apps/web/Dockerfile.web |
git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.<n> |
plane-space |
no | upstream makeplane/plane-space:v1.3.0 |
(no rebuild) |
plane-admin |
no | upstream makeplane/plane-admin:v1.3.0 |
(no rebuild) |
plane-live |
no | upstream makeplane/plane-live:v1.3.0 |
(no rebuild) |
plane-proxy |
no | upstream makeplane/plane-proxy:v1.3.0 |
(no rebuild) |
Tag scheme per architecture §6 #7: <upstream-version>-mine.<n>. Push immutable tag + :latest:
# from C:\Users\maxwe\GitHubRepos\bb-plane-fork
docker build -t git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.2 \
-t git.binarybeach.io/binarybeach/plane-backend:latest \
-f apps/api/Dockerfile.api .
docker push git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.2
docker push git.binarybeach.io/binarybeach/plane-backend:latest
docker build -t git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.2 \
-t git.binarybeach.io/binarybeach/plane-frontend:latest \
-f apps/web/Dockerfile.web .
docker push git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.2
docker push git.binarybeach.io/binarybeach/plane-frontend:latest
mine.<n> resets to mine.1 on every upstream version bump; increments per local rebuild within the same upstream version.
License compliance
Plane is AGPL-3.0-only. The license requires us to provide the source of any modified version we deploy or offer over a network. Our compliance:
- Forgejo source —
git.binarybeach.io/binarybeach/bb-plane-forkis publicly readable. - GitHub mirror — push-mirror to
github.com/binarybeachllc/bb-plane-fork. - In-product source link — TODO. AGPL §13 requires "prominent" notice to network users; a footer suffices. Tracked separately.
Test plan (manual, until we have CI)
- Local build smoke: both images build cleanly.
- Local stack:
docker compose -f docker-compose.bb-local.yml --env-file .env.bb-local up -d(withBB_BRIDGE_PUBLIC_KEY_URLunset) → vanilla email+password sign-in works (regression check). - Trusted-JWT happy path: with
BB_BRIDGE_PUBLIC_KEY_URLpointing at production bridge, hand-mint a JWT (claims:iss=bb-bridge,aud=plane, validexp, freshjti, valid email),GET /auth/sign-in-trusted/?token=<jwt>&next_path=/, expect 302 to/with sessionid cookie set. - Trusted-JWT replay rejection: hit the same URL with the same token twice. First → 302 + sessionid. Second → 302 to error redirect with
TRUSTED_JWT_TOKEN_REPLAYED. - Trusted-JWT disabled regression: unset
BB_BRIDGE_PUBLIC_KEY_URL, hit/auth/sign-in-trusted/, expect 404. - Production deploy: bump tag in
binarybeachio/infrastructure/plane/docker-compose.yml→py infrastructure/_shared/bootstrap.py→ verify onpm.binarybeach.binarybeach.io.