Skip to content

feat: add WALLET_OPERATIONS feature flag for the sunset - #1183

Merged
jbojcic1 merged 2 commits into
livefrom
feat/sunset-feature-flag
Sep 17, 2026
Merged

jbojcic1 merged 2 commits into
livefrom
feat/sunset-feature-flag

Conversation

@ditto-agent

Copy link
Copy Markdown
Contributor

Summary

Adds one positive feature flag, WALLET_OPERATIONS, that gates signup, send, receive and Lightning Address receives. true (the default) means the wallet works as today. For the sunset, the flag is switched to "enabled for nobody". Individual users can then be added to rules.user_ids so they can log in and withdraw.

The flag is positive because the existing rules engine (wallet.is_feature_enabled) can only turn a flag on for specific users. A negative "SUNSET" flag could not express per-user exemptions.

Runbook

-- deadline: disable for everyone (keep enabled = true)
update wallet.feature_flags set rules = '{"user_ids": []}' where key = 'WALLET_OPERATIONS';

-- let one user withdraw
update wallet.feature_flags
set rules = jsonb_set(rules, '{user_ids}', rules->'user_ids' || to_jsonb('<user id>'::text))
where key = 'WALLET_OPERATIONS';

-- undo
update wallet.feature_flags set rules = '{}' where key = 'WALLET_OPERATIONS';

Do not use enabled = false for the deadline: flipping it back on later without rules would re-open the wallet for everyone. Lightning Address receives stay off for exempted users too, because the server evaluates the flag without a user.

What changes while the flag is off

Signup

  • /signup shows a closed message and no signup buttons. Login page and login form hide the "Sign up" link.
  • signUp and guest creation in useAuthActions throw a DomainError (guards the action itself).
  • Marketing page: "Sign up" is hidden, "Get Started" becomes "Log in".
  • Public token claim page hides "Claim" / "Claim as Guest" and shows a message.
  • Database: a restrictive insert policy on wallet.users rejects new users. This also covers a first-time Google login, which otherwise creates a wallet through the protected layout. The layout catches the rejection, signs the auth user out and redirects to /home with a toast.

Send and receive

  • Home screen shows a notice; Receive and Send are disabled buttons without links. Buy is untouched.
  • clientMiddleware on _protected.send.tsx and _protected.receive.tsx redirects every route in those trees to / with a toast. This covers deep links, scan results, contacts, gift cards, transaction "pay again" links and the logged-in token claim page.
  • Send and receive cannot be enforced in the database: Spark payments settle through the Breez SDK and never touch it.

Lightning Address

  • LUD-16 (/.well-known/lnurlp/:username) and LUD-06 callback return {"status":"ERROR","reason":"Agicash is shutting down and no longer accepts payments."}.
  • LUD-21 verify keeps working so senders can still verify old payments.

Migration notes

  • supabase/migrations/20260917120000_add_wallet_operations_feature_flag.sql adds the flag row, the insert policy and a security definer helper wallet.user_exists(uuid).
  • Bug found while testing: Postgres evaluates insert policies on the proposed row before it detects the on conflict do update path, so a plain insert policy also fires for every existing user's login-time upsert. The existing Require email when GUEST_SIGNUP disabled policy has this flaw: with GUEST_SIGNUP off, existing guest users cannot load the wallet (verified locally). Both policies now pass when the user row already exists. Revert that part if you prefer to keep this PR narrow.
  • A policy cannot query its own table (Postgres reports infinite recursion), hence the helper function. Execute is revoked from public and anon.
  • No type regeneration needed: the RPC types already exist and the flag is data.
  • Deploy order: the client and the server treat a missing flag row as enabled, so the app can ship before or after the migration.

Verification

  • bun run fix:all, bun run typecheck (all packages) and bun run test (133 pass) are green. bun run build succeeds, including the prerendered /home.
  • Migration executed against the local database inside a rolled-back transaction: flag evaluation, all three runbook statements, existing user upsert during sunset, new user rejected through the real upsert_user_with_accounts RPC (SQLSTATE 42501), exempted new user accepted, existing guest accepted and new guest rejected with GUEST_SIGNUP off, anon cannot call user_exists.
  • Local dev server with a temporary flag row: LUD-16 and LUD-06 return the sunset error when off and the normal payRequest when on; /home shows only "Log in"; /signup shows the closed message; /login hides the signup link; logged-in home shows the notice with disabled Receive/Send; in-app navigation to /send redirects to / with the toast.
  • Not verified live: the Google-login rejection path (needs the applied policy plus a fresh Google account).

Not included (candidates for the same flag)

Buy (creates a receive invoice through Cash App), internal transfers between own accounts (/transfer/*), adding Cashu accounts and gift cards, and gift card offers. Each can reuse requireWalletOperations with one export line per layout route.

🤖 Generated with Claude Code

Gate signup, send, receive and Lightning Address receives behind one
positive flag so the wallet can be wound down on a deadline and single
users can be re-enabled afterwards to withdraw their funds.

- migration: WALLET_OPERATIONS row (enabled, rules {}), restrictive insert
  policy on wallet.users, security definer wallet.user_exists helper, and
  the GUEST_SIGNUP policy recreated with the same existing-user guard so
  it stops locking out existing guests
- client: closed signup page and guarded signup actions, home buttons
  disabled with a notice, client middleware on the /send and /receive
  route trees, public token claim disabled, marketing CTAs fall back to
  login, missing flag rows fall back to defaults
- server: LUD-16 and LUD-06 return a sunset error while the flag is off
- protected layout signs out a new user that the insert policy rejects
  and redirects to /home with a message

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agicash Ready Ready Preview Sep 17, 2026 7:24pm UTC

Request Review

@supabase

supabase Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Updates to Preview Branch (feat/sunset-feature-flag) ↗︎

Deployments Status Updated
Database ✅ Thu, 17 Sep 2026 19:24:34 UTC
Services ✅ Thu, 17 Sep 2026 19:24:34 UTC
APIs ✅ Thu, 17 Sep 2026 19:24:34 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations ✅ Thu, 17 Sep 2026 19:24:34 UTC
Migrations ✅ Thu, 17 Sep 2026 19:24:35 UTC
Seeding ✅ Thu, 17 Sep 2026 19:24:35 UTC
Edge Functions ✅ Thu, 17 Sep 2026 19:24:35 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jbojcic1
jbojcic1 merged commit a499157 into live Sep 17, 2026
6 checks passed
@jbojcic1
jbojcic1 deleted the feat/sunset-feature-flag branch September 17, 2026 19:57
<Button className="w-full px-7 py-6 text-lg">Send</Button>
</LinkWithViewTransition>
<ActionButton to="/send" disabled={!walletOperationsEnabled}>
Send

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be disabling send? I was thinking we just disable receive so that people can still withdraw?

This branch was successfully deployed

1 active deployment
Preview — d9000262 Deployed Sep 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants