Cohorts, grants, audit, and operator safety
Admin & Grandfathering Plan
Prepared 18 July 2026 · product requirements normalized from the placement artifact, current implementation status, and founder requirements
Goal
Let authorized operators grant Plus or Pro to selected legacy users for a defined period, preview the impact before writing, retain a complete audit trail, and never overwrite a user's paid App Store access.
Current admin baseline
The supplied implementation report says the user-level grant panel already supports effective access, Plus/Pro grant, 30/60/90-day shortcuts, reason, campaign, idempotency key, grant history, operator attribution, and revocation. It is on feat/billing-grants-admin, not main. What is missing is cohort preview and bulk execution.
Grandfather policy fields
| Field | Requirement |
|---|---|
| Policy/campaign | Stable slug such as legacy-plus-2026-v1 |
| Cohort | Signup before/after cutoff; optional last-active, country, app build, existing tier, internal/test exclusions |
| Granted plan | Plus or Pro |
| Start/end | Exact UTC timestamps with operator timezone preview |
| Permanent | Explicit boolean/type if approved; never a silent year-2099 workaround |
| Reason | Human-readable customer/support rationale |
| Exclusions | Deleted, banned, test, employee, refunded/fraud, opted-out, already higher paid access |
| Idempotency | Campaign + user unique key |
Operator workflow
- Create draft grandfather policy.
- Choose cohort filters and dates.
- Run read-only preview: total users, tier breakdown, paid/granted conflicts, exclusions, sample users.
- Export preview CSV and require second authorized approver above the configured threshold.
- Apply as a background batch with per-user idempotency.
- Show progress, success, skipped, failed, and retry counts.
- Verify random sample effective snapshots.
- Notify users only after product approves messaging.
- Monitor expiry/renewal and support incidents.
Admin surfaces
- User billing panel: retain current grant/revoke/history and add effective capability/quota summary.
- Grandfather campaigns list: draft, previewed, approved, running, completed, canceled.
- Campaign builder: cohort filters, plan, dates, reason, exclusions, notification choice.
- Dry-run report: counts, conflicts, samples, CSV export, estimated impact.
- Batch monitor: progress, failures, retry, pause/cancel before remaining writes.
- Audit view: actor, approver, timestamps, policy version, filters snapshot, grant IDs.
- Expiry dashboard: grants ending in 7/30 days and resulting underlying tier.
Authorization and safeguards
- Separate permissions:
billing.view,billing.grant.single,billing.grant.bulk,billing.revoke,billing.approve. - Bulk changes require reauthentication and two-person approval above threshold.
- Backend enforces roles; do not rely only on admin UI RBAC or one shared secret.
- No raw arbitrary SQL/audience query in the browser; use allow-listed filters.
- Grant never changes RevenueCat/App Store subscription state and never lowers higher paid access.
- Revocation/expiry bumps access revision so clients refresh promptly.
Backend/admin checklist
- Create cohort preview endpoint with immutable filter snapshot/hash.
- Create batch endpoint requiring preview ID, policy version, actor, approval token, and idempotency key.
- Create background worker with resumable cursor and per-user transaction.
- Create batch status and paginated result endpoints.
- Create revoke-campaign behavior that revokes only grants created by that campaign.
- Create extension workflow as a new audited grant or explicit extension event.
- Emit analytics/audit events without exposing sensitive data.
Grandfathering acceptance criteria
- Running the same campaign twice creates no duplicate effective grants.
- A paying Pro user included in a Plus campaign remains Pro.
- Grant expiry reveals valid paid access or Free correctly.
- Cohort preview count matches batch attempted + excluded totals.
- Every write identifies actor, approver, campaign, reason, and policy version.
- Support can answer “why does this user have access?” from one timeline.