← Back to home

Docs

Reference for PlanPush features, configuration, and admin capabilities.

Named URLs

Plans get shareable, human-readable URLs based on the session name you provide.

When you run /planpush name:auth-redesign, the plan is published at /p/auth-redesign instead of an auto-generated ID like /p/sess_064d4a62049b.

FormatExampleHow it's set
Named (preferred)/p/auth-redesignname: prefix or prompted on first push
Auto-generated/p/sess_064d4a62049bFallback when no name is given
Name collisions: If the name is already taken, the server returns HTTP 409. Choose a different name or update the existing session.

Visibility

Sessions have two visibility states: private and published.

  • Private — visible only to the owner and admins. Created by passing X-Visibility: private from the CLI.
  • Published — visible to all authenticated users in your org. This is the default.

Publishing is one-way: once a private session is published, it can't be made private again. Unauthorized access returns 404 (not 403) to prevent leaking session existence.

Dashboard: Admins and project managers see all private plans with a "Private" badge. Everyone else sees only their own private plans.

Version history

Every push creates a new version snapshot. Versions are retained for 90 days, then automatically deleted.

  • View older versions by appending ?v=N to the plan URL (e.g. /p/auth-redesign?v=2)
  • A version banner appears when viewing a non-latest version
  • Each version records who pushed it and when
Retention: Snapshots older than 90 days are automatically purged. The latest version is always kept.

Archiving

Sessions can be archived to keep the dashboard clean. Archived sessions are hidden from the default view but not deleted.

  • Toggle archive from the dashboard (owner or admin)
  • Archived sessions are soft-deleted (archived_at timestamp) and can be unarchived at any time

Comments

Comments are anchored to specific sections of a design doc. They survive when the plan is regenerated.

  • Click any section heading to leave a comment anchored to that element
  • Comment body is capped at 4,000 characters; anchor text at 200
  • Comments can be resolved and show in the sidebar alongside the plan
  • "New since last visit" badges appear on sessions that have new comments or updates since you last viewed them

Authentication

The web sign-in provider is selected with the AUTH_PROVIDER environment variable (default github):

  • GitHub OAuth (AUTH_PROVIDER=github) — standard OAuth flow, gated to a single GitHub org. The zero-config default for small teams.
  • Okta OIDC SSO (AUTH_PROVIDER=okta) — OpenID Connect (Authorization Code + PKCE) for enterprise single sign-on. See Okta SSO below.

Either provider works with the CLI device flow (RFC 8628): a code is shown in the terminal and you approve it in the browser — the browser step simply uses whichever provider is active. The device flow requires a web login first; if the plugin can't find your account it returns an error with the login URL.

One active provider at a time. When AUTH_PROVIDER=okta, the GitHub web-login routes are disabled so there's no path that bypasses Okta-enforced MFA. Identity is stored provider-agnostically, so an instance can be moved between providers without reshaping the user table.

Access gating

How a user is granted access depends on the active provider:

  • GitHub: access is restricted to members of a single GitHub organization, set via GITHUB_ORG. Users outside the org are rejected at sign-in.
  • Okta: access is governed by group → role mapping. A user whose Okta groups map to at least one role is admitted; a user who maps to no role is denied (no session created). Bootstrap admins are listed in INITIAL_ADMIN_EMAILS.

HTML sanitization

Pushed HTML is sanitized server-side to prevent XSS:

  • Inline scripts are stripped via cheerio
  • All inline scripts injected by the server use CSP nonces (base64url-encoded)
  • Plan CSS and JS are injected server-side, not authored by users

Rate limiting

All authentication endpoints are rate-limited to 30 requests per 15 minutes per IP via express-rate-limit.

Okta SSO (OIDC)

PlanPush signs users in through Okta using OpenID Connect (Authorization Code + PKCE). ID-token signatures are verified against Okta's JWKS with automatic key rotation; MFA is delegated to Okta.

1. Create the Okta application

In the Okta admin console, create an OIDC — Web Application (a confidential client) and configure:

Okta settingValue
Grant typeAuthorization Code (PKCE enabled)
Sign-in redirect URI{BASE_URL}/auth/callback
Initiate login URI{BASE_URL}/auth/initiate_login_uri (enables the Okta dashboard tile)
Sign-out redirect URI{BASE_URL} (or your POST_LOGOUT_REDIRECT_URI)

2. Configure PlanPush

Set the following in your .env and restart:

VariableDescription
AUTH_PROVIDERSet to okta to make Okta the web sign-in provider
OKTA_ISSUERYour Okta org/authorization-server issuer, e.g. https://your-org.okta.com
OKTA_CLIENT_IDClient ID from the Okta app
OKTA_CLIENT_SECRETClient secret from the Okta app
INITIAL_ADMIN_EMAILSComma-separated emails granted admin on first login (bootstrap)
Sign-in is SP-initiated. Users start at PlanPush (/auth/login) and are redirected to Okta; the registered initiate-login URI lets the Okta dashboard tile launch the same flow. There is no unsolicited-response ACS endpoint to harden.

Group → role mapping

With Okta active, access is managed by Okta group rather than per user. PlanPush reads the groups claim from the ID token and reconciles the user's roles on every login, so promotions and demotions in Okta propagate automatically.

Setup

  1. Add a groups claim to the Okta OIDC app (Sign-on → OpenID Connect ID Token), filtered to the groups you want PlanPush to see.
  2. Sign in once with a bootstrap admin (an address in INITIAL_ADMIN_EMAILS).
  3. In the dashboard's admin area, map each Okta group to a PlanPush role. Mappings are stored in the database and editable live — no redeploy needed.

Maintaining access

  • Grant/revoke access: change a user's Okta group membership — it takes effect on their next login.
  • Re-sync is non-destructive to manual grants: each role grant is tagged with an origin (sso or manual). Login reconciles only the sso grants to match the current group mapping; one-off manual grants made in the dashboard (break-glass / exceptions) persist.
  • Zero roles = denied: a user whose groups map to no role (and who isn't a bootstrap admin) sees an "access not yet granted" page and gets no session.
Bootstrap: INITIAL_ADMIN_EMAILS replaces the GitHub-mode "first user becomes admin" rule — it solves the chicken-and-egg of needing an admin before any group mappings exist.

SCIM provisioning

PlanPush exposes a SCIM 2.0 endpoint (built on scimmy) so Okta can auto-provision and, crucially, auto-deprovision users — the compliance control enterprises audit for.

Setup

  1. Set SCIM_AUTH_TOKEN to a strong random bearer token in your .env.
  2. In Okta, enable provisioning for the app with SCIM base URL {BASE_URL}/scim/v2 and the bearer token above. /Users is the priority resource; /Groups membership sync is accepted (role effect still flows through group → role mapping).
  3. Assign users/groups to the app in Okta — provisioning happens automatically.

Deprovisioning

When Okta sends a SCIM PATCH active=false (e.g. an employee is offboarded), PlanPush immediately:

  • marks the user deactivated, clears the deactivation cache, and
  • revokes the user's API-token family — so both their web sessions and CLI tokens stop working at once, not after the next token expiry.
Auth: the /scim/v2 endpoints are protected by the dedicated SCIM_AUTH_TOKEN bearer (constant-time check, fails closed if unset) — separate from user sessions.

Sessions & logout

Browser sessions are server-side and revocable (stored in the database via express-session), so access can be cut immediately rather than waiting for a signed cookie to expire.

  • Idle timeoutSESSION_IDLE_TIMEOUT (default 8h), a rolling window reset on each request.
  • Absolute lifetimeSESSION_MAX_AGE (default 7d), enforced against session creation time regardless of activity.
  • RP-initiated logout — logging out destroys the server-side session and (in Okta mode) redirects to Okta's end_session_endpoint, so the user is signed out of both PlanPush and Okta. Set POST_LOGOUT_REDIRECT_URI for the return destination (defaults to BASE_URL).
  • Forced revocation — deactivating a user (admin UI or SCIM) ends their sessions and tokens immediately.

Roles & permissions

Access is governed by a role/permission model with four built-in roles — admin, project manager, developer, and QA. Permissions are checked per request (with a short cache), so a role change takes effect right away rather than waiting for a new login. In Okta mode, roles are assigned via group → role mapping.

CapabilityAdminPMDeveloperQA
View published plansYesYesYesYes
Comment & resolveYesYesYesYes
Push (create) sessionsYesYesYes
Publish / archiveAnyAnyOwn
View others' private plansYesYes
View audit logYesYes
Delete sessionsYes
Manage users & rolesYes
Bootstrap: In Okta mode the first admins come from INITIAL_ADMIN_EMAILS; in GitHub mode the first person to sign in becomes admin. (Instances created before roles existed are migrated so the old member capability set maps to developer.)

User management

Admins can deactivate users from the Members tab in the dashboard. Deactivated users are blocked from signing in and their active tokens are invalidated.

  • Deactivation status is cached for 5 minutes to minimize DB lookups on every request
  • Last-admin protection: you can't deactivate the only admin or demote them to a non-admin role

Audit log

All significant actions are recorded in the audit log. Writes are fire-and-forget (non-blocking) to avoid slowing down requests.

Admins and project managers see the full activity feed in the dashboard; other roles see only their own activity.

API tokens

Users can create and manage API tokens from the dashboard's API Tokens tab. Tokens can be revoked individually. Revocation is immediate — the revoked_at timestamp is checked on every request.

Push endpoint

The Claude Code plugin pushes design docs via POST /api/push.

HeaderPurpose
X-Session-NameSet the session name and URL slug on first push
X-Session-IdUpdate an existing session on subsequent pushes
X-VisibilitySet to private to create a private session (default: published)
AuthorizationBearer token from the device flow

Device flow

The CLI authenticates using the RFC 8628 device authorization grant:

  1. Plugin requests a device code from GET /api/auth/device
  2. User opens the activation URL and enters the code in the browser
  3. Plugin polls POST /api/auth/device/token until approved
  4. Server returns a refresh token; plugin exchanges it for access tokens via POST /api/auth/token

Credentials are stored locally. Subsequent runs skip authentication entirely.

Slack integration

Set SLACK_WEBHOOK_URL to receive notifications in your Slack channel when:

  • A plan is updated with a new version (skipped for private plans)
  • A comment is posted (skipped for private plans)
  • A comment is resolved (skipped for private plans)
Security: User-generated content in Slack messages is escaped to prevent mrkdwn injection.

Docker

The official image is published to Docker Hub as frannsoftdev/planpush.

  • Tags: frannsoftdev/planpush:{version} and frannsoftdev/planpush:latest
  • Architecture: linux/amd64
  • Runs Node as a non-root planpush user via su-exec
  • Migrations run automatically on startup
  • Graceful shutdown on SIGTERM/SIGINT (drains connections, destroys DB pool)

See the Get Started guide for full deployment instructions.

Database

PlanPush uses Knex.js as a query builder (no ORM).

OptionDriverConfig
SQLite (default)better-sqlite3Zero-config — data stored in a file
PostgreSQLpgSet DATABASE_URL=postgres://...

Migrations are JavaScript files that run automatically at startup. The KV store used for caching is database-backed (not filesystem).

Environment variables

VariableRequiredDescription
SECRET_KEYYesRandom string, min 32 chars
BASE_URLYesPublic URL of your server
AUTH_PROVIDERNoWeb sign-in provider: github (default) or okta
GITHUB_CLIENT_IDgithubGitHub OAuth App client ID
GITHUB_CLIENT_SECRETgithubGitHub OAuth App client secret
GITHUB_ORGgithubGitHub org name for access control
OKTA_ISSUERoktaOkta issuer URL, e.g. https://your-org.okta.com
OKTA_CLIENT_IDoktaOkta OIDC app client ID
OKTA_CLIENT_SECREToktaOkta OIDC app client secret
INITIAL_ADMIN_EMAILSNoComma-separated bootstrap admin emails (Okta mode)
POST_LOGOUT_REDIRECT_URINoReturn URL after Okta single logout (defaults to BASE_URL)
SESSION_IDLE_TIMEOUTNoRolling idle timeout in seconds (default 28800 = 8h)
SESSION_MAX_AGENoAbsolute session lifetime in seconds (default 604800 = 7d)
SCIM_AUTH_TOKENNoBearer token enabling the SCIM 2.0 provisioning endpoint
PORTNoServer port (default: 3000)
DATABASE_URLNoPostgreSQL connection string (omit for SQLite)
SLACK_WEBHOOK_URLNoSlack incoming webhook for notifications
Conditional requirements: the github-marked vars are required only when AUTH_PROVIDER=github; the okta-marked vars only when AUTH_PROVIDER=okta. SECRET_KEY must be at least 32 characters — the server refuses to start if it's shorter.