Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLI
Assigneeskeeb

Relationships

#1544 swamp auth whoami should report paid tier and trial status

Opened by keeb · 8/6/2026· Shipped 8/7/2026

Problem

swamp auth whoami tells you who you are but not what you're entitled to. There is no way, from the CLI, to answer "am I on a paid plan?", "am I in a trial?", or "how many days of trial do I have left?" — the operative has to open the web UI (/orgs or /o/<slug>/billing) to find out.

This matters in three concrete places:

  • Entitlement-gated failures are unexplainable. Private extensions are gated on planEntitlesPrivateExtensions(plan) (plan !== "free"), but when a push or pull is refused the CLI has no local notion of plan to say why, or to suggest which collective needs upgrading.
  • Trial expiry is invisible. swamp-club runs a product-owned 30-day trial (TRIAL_DURATION_DAYS = 30, started by collective_created, serve_token_minted, or datastore_token_minted). Day 31 changes what the operative is told — but nothing in the CLI ever tells them.
  • Support and scripting. "Paste your swamp auth whoami --json" should be enough to triage a billing-adjacent report. Today it isn't.

Current state

CLI side (swamp):

  • src/cli/commands/auth_whoami.ts — the Cliffy command
  • src/libswamp/auth/whoami.ts:42WhoamiIdentity
  • src/infrastructure/http/swamp_club_client.ts:62-93WhoamiResponse, WhoamiOrganization
  • src/presentation/renderers/auth_whoami.ts — log + json renderers

GET /api/whoami returns { authenticated, id, username, email, name, organizations[] }, where each org is { slug, name, role, personal }. The CLI parses organizations and then throws away everything but slug (getCollectives() at swamp_club_client.ts:88-92), so log mode prints only Collectives: a, b. No plan, no trial, anywhere in the chain.

Server side (swamp-club) — billing is per-collective, not per-user; a user's paid status is derived from the subscriptions of the collectives they belong to:

  • Plan lives in Mongo billing_subscription, _id = collectiveId (lib/infrastructure/mongo-subscription-repository.ts:36)
  • PlanId = "free" | "team" | "business" | "enterprise" (lib/billing-plans.ts:16-38)
  • SubscriptionStatus mirrors Stripe; ENTITLING_STATUSES = ["active","trialing","past_due"], and effectivePlan() collapses to "free" when access isn't granted (lib/domain/billing/subscription.ts:46-64, 164-167)
  • The 30-day product trial is separate from Stripe's trialing: trialStartedAt / trialTrigger on the organization_profile doc; TrialStatus (state: none|active|expired, startedAt, endsAt, dayNumber, daysRemaining) is derived on read, never persisted (lib/trial.ts:26,40-48,60-89)

routes/api/whoami.ts:73-78 deliberately projects only slug/name/role/personal.

Proposed solution

1. Server: extend GET /api/whoami

Add per-collective entitlement to each entry in organizations, plus a top-level roll-up for the common "am I paid?" question:

{
  "authenticated": true,
  "id": "...", "username": "...", "email": "...", "name": "...",
  "plan": "team",                      // highest plan across the user's collectives
  "organizations": [
    {
      "slug": "acme", "name": "Acme", "role": "admin", "personal": false,
      "plan": "team",                  // effectivePlan(): "free" unless grantsAccess()
      "planName": "Team",
      "subscriptionStatus": "active",  // Stripe status, or null when no subscription
      "trial": {                       // omitted/null when state === "none"
        "state": "active",             // none | active | expired
        "startedAt": "2026-07-20T00:00:00.000Z",
        "endsAt":    "2026-08-19T00:00:00.000Z",
        "daysRemaining": 13
      }
    }
  ]
}

Two notes on implementation cost:

  • Trial data is free. handleGetWhoami already calls listUserCollectivesWithRoles(userId) (routes/api/whoami.ts:64-65), which returns CollectiveData & { role } — and CollectiveData already carries trialStartedAt / trialTrigger via the organization_profile join (lib/infrastructure/better-auth-collective-queries.ts:318-338). Just run toTrialStatusView(o) (lib/app/views/organization-view.ts:24-37) over what's already loaded. Zero extra queries.
  • Plan costs one batched read. Add subscriptionRepo to WhoamiDeps (routes/api/whoami.ts:11-14) and call findManyByCollectiveIds(orgIds) (lib/repositories/subscription-repository.ts:17-19).

Precedents to copy rather than reinvent:

Need Existing code
Highest plan across a user's collectives lib/app/lab/enrich-author-plans.ts:30-146 (TIER_RANK, batched lookup, 60s TTL cache, clearPlanCache() on sub change)
Serializable subscription projection toSubscriptionView (lib/app/views/billing-view.ts:14-63)
Paid plan must supersede the trial clock lib/app/collective-page-context.ts:80-96 — "a collective that is paying must never be told it is on a free trial"

Collective API tokens (routes/api/whoami.ts:85-101) should get the same fields for their single owning collective.

The additions are purely additive, so older CLIs keep working.

2. CLI: surface it

  • WhoamiResponse / WhoamiOrganization (swamp_club_client.ts:62-93) gain the new optional fields.

  • WhoamiIdentity (libswamp/auth/whoami.ts:42) carries plan and a per-collective entitlement list rather than a bare string[] of slugs. This means retiring the lossy getCollectives() flattening — the auth.json cache and the collectives field need to keep their current shape for compatibility, so the richer data should ride alongside rather than replace it.

  • log mode:

    keeb ([REDACTED-EMAIL]) on https://swamp-club.com
    Plan: Team
    Collectives:
      acme    Team       active
      keeb    Free       trial: 13 days left (ends 2026-08-19)
  • json mode: pass the fields through verbatim.

All fields must be optional on the CLI side — a self-hosted or older swamp-club won't send them, and whoami must not regress to an error in that case.

Alternatives considered

  • A separate swamp auth plan / swamp billing status command. More surface for the same single round-trip; whoami is already the "what does the server think of me" call, and it already fetches the org list this would need.
  • Resolve plan CLI-side from a billing endpoint. Requires a second authenticated request and duplicates effectivePlan() / trial-derivation logic in the CLI, where it will drift. The server owns entitlement (see also the "server is authoritative" principle for telemetry identity) — it should own this answer too.
  • Only expose the top-level roll-up, not per-collective. Insufficient: the actionable question when a private-extension push is refused is which collective needs upgrading, which a single roll-up can't answer.

Out of scope

Making the 30-day trial actually grant entitlement. Today it grants none (lib/trial.ts:29-33) — this issue is about reporting status, not changing it.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 5 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

8/7/2026, 12:32:09 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
keeb assigned keeb8/6/2026, 6:31:05 PM

Sign in to post a ripple.