Skip to content

feat(sunset): wind down the hosted service behind SUNSET_MODE - #1801

Merged
CREDO23 merged 6 commits into
MODSetter:devfrom
CREDO23:sunset-fixes
Sep 17, 2026
Merged

CREDO23 merged 6 commits into
MODSetter:devfrom
CREDO23:sunset-fixes

Conversation

@CREDO23

@CREDO23 CREDO23 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #1777, which had drifted far enough from dev that its diff no longer rendered. Same work, rebuilt on the current base, with three bugs fixed. Credit for the implementation is @AnishSarkar22's.

What it does

One flag, SUNSET_MODE, read per request on both sides, turning the hosted service export-only at T-0. Unset means off, so self-hosted installs are unaffected — that half is a launch gate, and the tests assert it path by path and method by method.

SunsetWriteBlockMiddleware refuses writes with 410. Reads are never touched, which is what keeps export working without anyone enumerating the routes that mutate. Auth stays open except /auth/register, because export lives behind a session. Licence routes and the Stripe webhook stay open, because that business outlives the wind-down.

proxy.ts sends app routes to /sunset while the portal stays reachable. Deliberately not NEXT_PUBLIC_*: those are inlined at build time, and the flip has to be a restart rather than a redeploy, the same way the backend reads its half.

/health reports sunset and now accepts 1, true, yes, on. The previous comparison was == "true", so contract 4's documented SUNSET_MODE=1 read as false — the switch would have looked thrown and done nothing.

Also: purge_hosted_accounts.py for T+30, looping the existing erase_account so blobs go with the rows; both runbooks; the flag documented in three .env.example files and both compose files.

Fixed relative to #1777

The scraper API and MCP were blocked at T-0. Scraper calls are POST /api/v1/workspaces/{id}/scrapers/{platform}/{verb}, which matched no allowlist prefix, so flipping the flag would have 410d the one product meant to survive the sunset and become the paid plugin at T+7. The workspace id sits mid-path, so a prefix cannot express it:

_ALLOWED_PATTERNS = (re.compile(r"^/api/v1/workspaces/\d+/scrapers/"),)

Covered by a test; the previous suite mentioned scrapers nowhere.

/downloads and /plugins were dropped from the public route list. Extracting PUBLIC_ROUTE_PREFIXES into lib/public-routes.ts moved a copy that predated those two entries. With the flag off that breaks both pages for anonymous visitors — ZeroProvider stops short-circuiting, the context fetch 401s, and handleUnauthorized sends them to /login. With the flag on, /downloads redirects to /sunset, whose whole purpose is to send people to /downloads. The extracted list is now byte-identical to the one on dev.

UpdateButton threw on click. It called bridge(), which dev had renamed to updatesBridge. Both sides merged cleanly as text, so only the test caught it.

The middleware-order comment is also corrected: add_middleware inserts at index 0, so the block runs inside CORS, not outside it — which is what lets a refused write carry the headers the browser needs to read it.

Checks

Backend sunset and licence suites pass, 186 tests. sunset-redirect and the desktop update tests pass. The desktop suite has 5 pre-existing failures, all of which also fail on a clean dev checkout (which has 6).

High-level PR Summary

This PR implements a coordinated wind-down mechanism for the hosted service behind a single SUNSET_MODE flag. When enabled, the backend refuses writes with 410 Gone while keeping exports and authentication working, and the web app redirects all application routes to /sunset while preserving the portal pages (licensing, pricing, downloads). The flag is read per-request on both sides so the switch is a restart rather than a redeploy, and it defaults to off so self-hosted installations are completely unaffected. The implementation includes a purge script for T+30 that loops the existing account deletion function to properly clean up blobs and knowledge stores, comprehensive test coverage asserting the launch gate path-by-path, and operational runbooks for both the sunset and purge stages. Three bugs from the original PR are fixed: the scraper API and MCP are now properly allowlisted, /downloads and /plugins are restored to the public route list, and UpdateButton calls the correctly-renamed bridge function.

⏱️ Estimated Review Time: 30-90 minutes

💡 Review Order Suggestion
Order File Path
1 plans/community-local/00d-pivot-plan.md
2 plans/community-local/sunset-runbook.md
3 plans/community-local/purge-runbook.md
4 surfsense_backend/app/sunset.py
5 surfsense_web/lib/sunset.ts
6 surfsense_web/lib/public-routes.ts
7 surfsense_backend/app/app.py
8 surfsense_web/proxy.ts
9 surfsense_backend/scripts/purge_hosted_accounts.py
10 surfsense_backend/tests/unit/test_sunset_flag.py
11 surfsense_backend/tests/unit/test_sunset_write_block.py
12 surfsense_web/tests/unit/sunset-redirect.test.ts
13 surfsense_backend/.env.example
14 surfsense_web/.env.example
15 docker/.env.example
16 docker/docker-compose.yml
17 docker/docker-compose.dev.yml
18 surfsense_web/lib/auth-utils.ts
19 surfsense_local/frontend/src/features/updates/update-settings.tsx
20 surfsense_local/frontend/src/features/updates/update-settings.test.tsx
21 surfsense_local/frontend/src/features/dashboard/dashboard-page.tsx

Need help? Join our Discord

@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: e4b09c36-65aa-48b9-9133-0518a5402fdf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CREDO23
CREDO23 merged commit e0138dc into MODSetter:dev Sep 17, 2026
11 of 18 checks passed
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.

2 participants