# 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--fork (where we push) github mirror → github.com/binarybeachllc/bb--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/ — 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.md` for the bucket taxonomy and bridge contract - `binarybeachio/docs/architecture/bridge-jwt-replay-protection.md` for the JWT replay-protection contract - `binarybeachio/docs/services/plane/migration-plan.md` for 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, 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 `RS256` JWT signed with `BRIDGE_SIGNING_KEY` (private). Claims: `iss=bb-bridge`, `aud=plane`, `iat`, `exp` (now+60s), `jti` (UUIDv4), `sub`, `email`, `first_name`, `last_name`, `tenant`. - Bridge 302s the user's browser to `https://pm..binarybeach.io/auth/sign-in-trusted/?token=&next_path=`. - Plane's view: fetches public key from `BB_BRIDGE_PUBLIC_KEY_URL` (cached 5 min), verifies signature + claims, atomically `SETNX bb_bridge_jti:` in shared-redis with TTL = `exp - now + 30s`, find-or-creates User by email, calls `user_login()`, 302s to `next_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 ` 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=&...`. 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.py` and 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 sibling `views/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`): ```bash # 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-:3000/.well-known/bb-bridge.pub.pem ``` Bridge-side configuration (in `binarybeachio/infrastructure/auth-bridge/.env`): ```bash 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-admin` user is seeded with a permanent password from `_shared/.env.bb-admin`. ## Cross-fork conventions adopted This fork pulls in binarybeachio's [session lifecycle convention](https://git.binarybeach.io/binarybeach/binarybeachio-platform/src/branch/main/docs/features/session-lifecycle.md) — 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: ```bash 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.` | | `plane-frontend` (aka web) | YES (Patch 2 frontend bits only) | `apps/web/Dockerfile.web` | `git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.` | | `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: `-mine.`. Push immutable tag + `:latest`: ```bash # 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.` 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: 1. **Forgejo source** — `git.binarybeach.io/binarybeach/bb-plane-fork` is publicly readable. 2. **GitHub mirror** — push-mirror to `github.com/binarybeachllc/bb-plane-fork`. 3. **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) 1. **Local build smoke**: both images build cleanly. 2. **Local stack**: `docker compose -f docker-compose.bb-local.yml --env-file .env.bb-local up -d` (with `BB_BRIDGE_PUBLIC_KEY_URL` unset) → vanilla email+password sign-in works (regression check). 3. **Trusted-JWT happy path**: with `BB_BRIDGE_PUBLIC_KEY_URL` pointing at production bridge, hand-mint a JWT (claims: `iss=bb-bridge`, `aud=plane`, valid `exp`, fresh `jti`, valid email), `GET /auth/sign-in-trusted/?token=&next_path=/`, expect 302 to `/` with sessionid cookie set. 4. **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`. 5. **Trusted-JWT disabled regression**: unset `BB_BRIDGE_PUBLIC_KEY_URL`, hit `/auth/sign-in-trusted/`, expect 404. 6. **Production deploy**: bump tag in `binarybeachio/infrastructure/plane/docker-compose.yml` → `py infrastructure/_shared/bootstrap.py` → verify on `pm.binarybeach.binarybeach.io`.