bb-plane-fork/BINARYBEACHIO.md
binarybeach 2a78f0e0ce binarybeachio: repurpose GitHub OAuth as Zitadel OIDC
Patches the plane-backend GitHubOAuthProvider so the /auth/github/*
flow points at our self-hosted Zitadel instance when ZITADEL_DOMAIN
is set, and falls back to vanilla GitHub OAuth when unset (regression-
safe). Touch surface is one backend file plus a cosmetic frontend
label change. Full rationale, configuration steps, refresh procedure,
and AGPL compliance notes in BINARYBEACHIO.md at repo root.
2026-04-29 16:50:40 -10:00

9.6 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 — 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):

# 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=<zitadel-app-client-id>
GITHUB_CLIENT_SECRET=<zitadel-app-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:

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.<n>
plane-frontend (aka web) YES 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)

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: <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.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.<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:

  1. Forgejo sourcegit.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.ymlpy infrastructure/_shared/bootstrap.py → verify on pm.binarybeach.io.