Skip to content

Backend: temporary per-user BASE_URL override for dev/preview testing#8787

Closed
aapeliv wants to merge 3 commits into
developfrom
backend/feature/base-url-override
Closed

Backend: temporary per-user BASE_URL override for dev/preview testing#8787
aapeliv wants to merge 3 commits into
developfrom
backend/feature/base-url-override

Conversation

@aapeliv

@aapeliv aapeliv commented May 24, 2026

Copy link
Copy Markdown
Member

Adds a dev/testing mechanism to override BASE_URL per user for ~15 minutes, so a frontend running on a Vercel preview or a mobile dev build (pointed at the shared stage backend) gets the links the backend generates — notifications, emails, redirect URLs, invite/verification links, etc. — back at whatever frontend they're testing on, rather than the configured BASE_URL.

How it works:

  • New append-only base_url_overrides table (user_id -> base_url history). The active override for a user is the most recent row within BASE_URL_OVERRIDE_TTL (15 min); an empty base_url is an explicit clear.
  • urls.py resolves BASE_URL through a ContextVar that falls back to config["BASE_URL"]. The override is set for the duration of a request (from the authenticated user, in the gRPC interceptor) and for a notification job (from the recipient, in handle_notification).
  • Two ENABLE_DEV_APIS-gated debug RPCs on the Notifications service: SetBaseUrlOverride (records a row) and GetBaseUrlOverrides (returns the history, newest first, with the active one flagged).
  • The whole thing is gated by ENABLE_DEV_APIS, so it's completely inert in real prod (no rows can be created or applied, and urls.py always uses BASE_URL). Intended to be enabled on stage, which otherwise runs prod config.
  • Auto-rewire from the clients (non-prod only, on mount + 10 min refresh): web BaseUrlOverrideSync sends window.location.origin; mobile useSetBaseUrlOverride sends EXPO_PUBLIC_WEB_BASE_URL.

Note: the two debug RPCs live on the Notifications service alongside the other ENABLE_DEV_APIS RPCs, even though the override is broader than notifications — happy to move to a dedicated dev service if preferred.

Testing

  • make format clean; make mypy clean for changed files (the 2 reported errors are pre-existing in untouched files: tests/fixtures/misc.py, test_calendar_events.py).
  • Migration-vs-model consistency test (test_db.py) passes, confirming 0158 matches the model and ordinals stay linear.
  • Backend suites pass: notifications, email, account, auth, donations, bugs, admin, db. Added tests covering the contextvar resolution, TTL/clear/expiry semantics, both debug RPCs, ENABLE_DEV_APIS gating, and end-to-end notification link rewriting (with and without an override).
  • Web: tsc --noEmit and eslint clean on changed files.
  • Mobile: changes mirror existing patterns; lint/types/tests build protos and run in CI (couldn't run locally — no node_modules/generated proto stubs).

To try on stage: set ENABLE_DEV_APIS=1, open a preview frontend authenticated as a user, and notifications/emails for that user will link back to the preview origin for 15 min.

Backend checklist

  • Added tests for any new code or added a regression test if fixing a bug
  • Run the backend locally and it works
  • Added migrations if there are any database changes, rebased onto develop if necessary for linear migration history

Web frontend checklist

  • There are no console warnings when running the app
  • Added tests where relevant
  • Clicked around my changes running locally and it works
  • Checked Desktop, Mobile and Tablet screen sizes

For maintainers

  • Maintainers can push commits to my branch
  • Maintainers can merge this PR for me

This PR was created with the Couchers PR skill.

@vercel

vercel Bot commented May 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
couchers Ready Ready Preview May 24, 2026 7:50pm

Request Review

@couchersbot

couchersbot Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @aapeliv's task in 1m 8s —— View job


Migration Review

One of our most common issues is migrations that don't work with existing data in production. This is very hard to catch with CI, so we have this bot to check for potential problems.

Overall Risk: SAFE

Files Reviewed

  • 0158_add_base_url_overrides.py — creates a new base_url_overrides table (append-only user_id -> base_url history) with an FK to users.id and a non-unique index on user_id.

Findings

No issues found.

  • It's a newly added migration file (new file mode 100644), not a modification of an already-applied one.
  • It creates a brand-new table that does not exist in the production schema, so there is no existing data to backfill, cast, or violate constraints. The NOT NULL columns and the users.id FK can't conflict with anything on an empty table, and the index builds instantly.
  • The migration matches the BaseUrlOverride model exactly (columns, types, nullability, FK name, index).
  • created uses server_default=sa.text("now()"), consistent with the project's UTC convention.
  • The revision chain is linear and correct (0158 revises 0157, the current head).
  • downgrade() cleanly reverses the upgrade (drops the index, then the table). Dropping a dev-only table on downgrade is the expected behavior.

Summary

Pure additive migration creating a new, empty table — no risk to existing production data.
· backend/feature/base-url-override

@couchersbot

couchersbot Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @aapeliv's task in 55s —— View job


Migration Review

One of our most common issues is migrations that don't work with existing data in production. This is very hard to catch with CI, so we have this bot to check for potential problems.

Overall Risk: SAFE

Files Reviewed

  • 0158_add_base_url_overrides.py — creates a new append-only base_url_overrides table (user_id -> base_url history) with a users.id FK and a non-unique index on (user_id, created).

Findings

No issues found.

  • New file, not a modification of an already-applied migration (new file mode 100644).
  • Creates a brand-new table that does not exist in the production schema (confirmed absent from schema.sql), so there is no existing data to backfill, cast, or violate. NOT NULL columns, the FK, and the index all build instantly on an empty table.
  • FK types are compatible: user_id is BigInteger and users.id is bigint NOT NULL.
  • Migration matches the BaseUrlOverride model exactly (columns, types, nullability, FK name, and the ix_base_url_overrides_active index on (user_id, created)).
  • created uses server_default=sa.text("now()"), consistent with the project's UTC convention.
  • Revision chain is linear and correct: 0158 revises 0157, the current head.
  • downgrade() cleanly reverses the upgrade (drops the index, then the table).

Summary

Pure additive migration creating a new, empty table — no risk to existing production data.
· backend/feature/base-url-override

aapeliv and others added 3 commits May 24, 2026 14:53
Replace the implicit reliance on config["BASE_URL"] in couchers.urls with an
explicit context: every base_url-derived helper now takes a CouchersContext as
its first positional arg and builds from a new context.base_url property
(currently just config["BASE_URL"]). This threads context through the servicer,
notification render, email, tasks and postal layers so that a later per-request /
per-recipient BASE_URL override becomes a small change to one property. The
media/console helpers are unchanged as they are not base_url-derived.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ting

Lets a dev/preview frontend tell the (shared stage) backend to point every
link it generates (notifications, emails, redirect URLs, ...) back at the
frontend they're testing on, instead of the configured BASE_URL. Backed by an
append-only user->base_url history table; the active override is the most
recent row within a 15 min TTL. Resolved through a contextvar that urls.py
reads, set per-request (authenticated user) and per-notification (recipient).
Gated by ENABLE_DEV_APIS so it is inert in real prod.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address review feedback on the per-user BASE_URL override:
- Resolution moves onto CouchersContext.use_base_url_override (a contextmanager
  that sets the urls contextvar), replacing the free-floating
  use_base_url_override_for_user(session, user_id) helper. The interceptor and
  notification job now go through context, matching how the rest of the codebase
  carries per-operation user state.
- An override is always non-empty: SetBaseUrlOverride rejects an empty base_url
  and the empty-string-clears-the-override semantics is gone (it expires via TTL).
- Add a composite index (user_id, created) serving the active-override lookup,
  replacing the redundant single-column user_id index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant