# 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 — push-mirror to GitHub satisfies this) | ## Why we forked 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 (verified 2026-04-29 against pm.binarybeach.io). We don't want to pay $338/mo for a single binarybeachio operator's SSO. We DO want every self-hosted service to authenticate users via the same Zitadel IdP (the break-glass admin convention from `binarybeachio/docs/architecture/self-hosting-infrastructure.md` §6.1 demands it). Plane's backend has working **community-edition** GitHub OAuth (`/auth/github/...`). We repurpose that flow to point at Zitadel by env-driving the four GitHub URL constants and switching the userinfo claim mapping to OIDC standard. This is described in detail in `apps/api/plane/authentication/provider/oauth/github.py`'s header comment. ## What's customized (the inventory — keep current) Touch surface is intentionally minimal. **Two files**, both narrowly scoped, designed to minimize merge conflict probability on every Plane upgrade. | File | Change | Risk on upgrade | |---|---|---| | `apps/api/plane/authentication/provider/oauth/github.py` | Repurposed entire file: env-drive endpoint URLs (default to `$ZITADEL_DOMAIN`'s OIDC endpoints, fall back to GitHub when `ZITADEL_DOMAIN` unset). Switch claim mapping to OIDC standard. Drop `__get_email` (OIDC userinfo includes email). Fix upstream's `expires_in` epoch-vs-duration bug. Drop `is_user_in_organization` (Zitadel handles authz). | **Medium.** This file rarely changes upstream. If Plane refactors the OauthAdapter base class signatures, our patched constructor must follow. | | `apps/web/core/hooks/oauth/core.tsx` | Cosmetic: rename "GitHub" button text to "binarybeach.io". Backend ID/route/icon path unchanged. | **Low.** Pure cosmetic; rebases trivially. | Files **not** changed (deliberately): - `apps/api/plane/authentication/views/app/github.py` — view layer, unchanged. Routes still `/auth/github/`. - `apps/api/plane/authentication/views/space/github.py` — public-share OAuth, unchanged. - `apps/api/plane/authentication/urls.py` — URL routing unchanged. - `apps/admin/...` — god-mode UI still says "GitHub" provider; only the operator (us) sees it, not worth the patch surface. - `apps/space/...` — public sharing site OAuth, not a priority for v1. ## Required runtime config Set these env vars on the patched `plane-backend` container (binarybeachio sets them in `infrastructure/plane/.env`): ```bash # Pin our Zitadel host — this enables the OIDC code path. Without it, the # patched provider falls back to vanilla GitHub OAuth (deliberate). ZITADEL_DOMAIN=auth.binarybeach.io # Optional explicit overrides if endpoints differ from Zitadel defaults. # Defaults derive from ZITADEL_DOMAIN: /oauth/v2/{authorize,token}, /oidc/v1/userinfo. # OIDC_AUTH_URL= # OIDC_TOKEN_URL= # OIDC_USERINFO_URL= # Existing Plane env vars (kept names — backend still calls them GITHUB_*) GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= ``` And in Plane's god-mode admin UI (`/god-mode/authentication/github`): - Toggle GitHub OAuth ON - Paste the same client_id/secret (god-mode DB rows shadow env vars at runtime — both must agree) In Zitadel: - Create OIDC Web application - Redirect URI: `https://pm.binarybeach.io/auth/github/callback/` (production) and `http://localhost/auth/github/callback/` (local test) - Auth method: client_secret_post (Plane sends creds in body) - Grant types: Authorization Code + Refresh Token - Response types: code ## 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 # resolve any conflicts (likely in github.py) # Run all tests, hand-test the OIDC flow against staging Zitadel # 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 | `apps/api/Dockerfile.api` | `git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.` | | `plane-frontend` (aka web) | YES | `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) | The binarybeachio compose file at `infrastructure/plane/docker-compose.yml` mixes our patched images with upstream-vanilla images for the four we don't touch. 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.1 \ -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.1 docker push git.binarybeach.io/binarybeach/plane-backend:latest docker build -t git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.1 \ -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.1 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 a public-readable repository (Forgejo `DEFAULT_PRIVATE=public`). 2. **GitHub mirror** — push-mirror to `github.com/binarybeachllc/bb-plane-fork` provides off-site backup AND a publicly-discoverable source location even if Forgejo is unreachable. 3. **In-product source link** — TODO: add a footer link in our customized `apps/web` to https://git.binarybeach.io/binarybeach/bb-plane-fork. AGPL §13 requires "prominent" notice to network users; a footer suffices. The TODO in #3 is tracked in the parent binarybeachio repo's compliance log when we get there. Not a v1 blocker — Plane already includes upstream license notices and our changes preserve them. ## Test plan (manual, until we have CI) 1. **Local build smoke**: both images build cleanly with current Dockerfiles. 2. **Local stack**: `docker compose -f docker-compose-local.yml up -d` (using patched images), pointed at hosted Zitadel. 3. **OIDC flow**: visit `http://localhost`, click "Continue with binarybeach.io", redirected to `auth.binarybeach.io`, log in as Zitadel user, redirected back, account auto-provisioned in Plane, signed in. 4. **New-user flow**: sign in with a Zitadel user that doesn't yet exist in Plane → Plane auto-creates the account. 5. **Re-login**: sign out, sign in again with same Zitadel user → matched by email, same Plane user. 6. **Fallback**: unset `ZITADEL_DOMAIN` env var, restart backend, try GitHub OAuth flow with real GitHub creds → should still work (regression check that we didn't break upstream behavior). 7. **Production deploy**: bump tag in binarybeachio `infrastructure/plane/docker-compose.yml` → `py infrastructure/_shared/bootstrap.py` → verify on `pm.binarybeach.io`.