Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ DASH_LEAFLET2_BASE_URL=http://localhost:8050
# Production primary (2plot.ai on its custom Clerk domains):
# CLERK_SIGN_IN_URL=https://accounts.2plot.ai/sign-in
# CLERK_SIGN_UP_URL=https://accounts.2plot.ai/sign-up
#
# THE RETURN TRIP — required in production, and the one Clerk setting that
# fails with no error anywhere. Set, the Sign In button navigates to
# https://2plot.ai/onboarding?returnTo=<this page>, a page on the hub's own
# app that validates returnTo and sends the user home. Unset, sign-in falls
# back to the Clerk-hosted Account Portal, the hub's app never sees the
# request, and the user authenticates successfully and stays on 2plot.ai.
# A DESTINATION, not a flag. It must be an absolute http(s) URL — the value
# is concatenated as `<value>?returnTo=<here>`, so `true` or a bare host
# resolves against THIS site and the button 404s, which is worse than
# leaving it unset. dash-clerk-auth only logs a warning and uses a bad
# value anyway; lib/auth.py warns at boot for both mistakes.
# Leave it unset LOCALLY (localhost is not a satellite); render.yaml sets
# it in production.
# CLERK_SATELLITE_SIGN_IN_REDIRECT=https://2plot.ai/onboarding
# CLERK_FRONTEND_API=https://clerk.2plot.ai
# CLERK_IS_SATELLITE=false
# SESSION_SECRET=
Expand Down
82 changes: 80 additions & 2 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ is logged.
| `CLERK_SATELLITE_SIGN_IN_REDIRECT` | (unset) | Optional, dash-clerk-auth ≥ 0.9.2. Absolute URL on the **primary** that Sign In navigates to, with this page in `?returnTo=`. Read by the package itself. Unset, sign-in falls back to `Clerk.redirectToSignIn()` forcing this page as the return — what this site ships today. Only set it once `2plot.ai` honours `?returnTo=`. |
| `SESSION_SECRET` | (generated) | Signs the session + `__dca_identity` cookies. Without it dash-clerk-auth uses a **public dev default**. |
| `ADMIN_EMAILS` | `a@b.com,c@d.com` | Allowlist for `/admin/control-board`. `OWNER_EMAIL` always counts. |
| `CLERK_SATELLITE_SIGN_IN_REDIRECT` | — | **Required in production.** `https://2plot.ai/onboarding`. Unset, sign-in reaches the primary and never returns — see the next section. |
| `DISABLE_CLERK` | `1` | Dev kill switch — reads as "intentionally off" without touching the keys. Never set in production. |
| `ALLOW_UNGATED_ADMIN` | `1` | Lets `/admin/control-board` render without Clerk. **Never set in production.** |

Expand Down Expand Up @@ -183,6 +184,81 @@ instance and cannot host satellites):
All five are already literals in `render.yaml`; only `CLERK_SECRET_KEY` and
`CLERK_PUBLISHABLE_KEY` need entering in the dashboard.

### The sign-in return trip — the failure with no error

**Symptom:** the visitor clicks Sign In, authenticates successfully on the
primary, and is left there. Nothing is logged on either host, no console error,
no failed request. Observed live on this service 2026-08-20.

**Cause:** `CLERK_SATELLITE_SIGN_IN_REDIRECT` unset. There are two paths, and
only one of them belongs to this network:

| | Set to `https://2plot.ai/onboarding` | Unset (the fallback) |
|---|---|---|
| Where the button goes | `2plot.ai/onboarding?returnTo=<this page>` | `CLERK_SIGN_IN_URL` → `accounts.2plot.ai`, Clerk's **hosted** Account Portal |
| Who validates the return | 2plot.ai's own Dash app, against `lib.auth.allowed_redirect_origins()` | Clerk's dashboard allowed-redirect list |
| Sign-up button | `&mode=signup` opens the sign-**up** modal | no equivalent |
| When it breaks | the hub's whitelist rejects the origin — visible in that repo | ClerkJS silently drops `signInForceRedirectUrl` and uses the portal default |

The fallback is why this is so quiet: the hub's Dash app is **never in the
loop**, so none of its `returnTo` machinery — the whitelist check, the
force-redirect, the auto-open — ever executes. `accounts.2plot.ai` is Clerk's
property, not ours.

**It is a destination, not a flag.** The value must be an absolute `http(s)`
URL, because `buildSatelliteRedirect()` builds `<value>?returnTo=<here>` by
concatenation. A truthy non-URL is the worst of the three states:

| Value | Where the Sign In button goes |
|---|---|
| `https://2plot.ai/onboarding` | `https://2plot.ai/onboarding?returnTo=…` — correct |
| *(unset)* | Clerk Account Portal — the stranding above |
| `true`, or a bare `2plot.ai/onboarding` | `https://leaflet.2plot.dev/true?returnTo=…` — resolved against **this** host, 404, sign-in impossible |

`dash-clerk-auth` does not reject a bad value: it emits one `logger.warning`
and uses it regardless. `lib/auth.py` therefore prints its own `[auth] ⚠️`
line for both mistakes, alongside the rest of the boot diagnostics.

`dash-clerk-auth` reads the variable from the environment itself, so
`lib/auth.py` does not pass it through; setting it on the service is the whole
fix. `lib/auth.py` now **warns at boot** whenever satellite mode is on and this
is unset, because the original note here said to set it "once 2plot.ai has a
page that honours `?returnTo=`" — 2plot.ai gained that page, and nobody came
back.

**The other half to check when this misbehaves** lives in the *2plotai* repo,
not here: `allowed_redirect_origins()` is env-first, so if
`CLERK_ALLOWED_REDIRECT_ORIGINS` is set on the hub's service it **replaces** the
built-in list rather than extending it. `https://leaflet.2plot.dev` is in the
built-in default; confirm it survived any override.

### Blueprint vs dashboard drift

`render.yaml` declares the contract; Render applies `envVars` on a **blueprint
sync**, not on an autoDeploy from a git push. So the two drift, silently, and
the code cannot tell. As measured on 2026-08-20 the live service was missing:

| Missing on the service | Consequence |
|---|---|
| `CLERK_SATELLITE_SIGN_IN_REDIRECT` | The sign-in return trip above. **Fix this one first.** |
| `PAGE_VISIBILITY_FILE` | Control-board overrides land on the container filesystem instead of `/var/data`, so every toggle resets on the next deploy. |
| `AD_SERVER_URL` | The ad slot never renders (the client's circuit breaker just keeps it hidden). |
| `ANALYTICS_GEO_LOOKUP` | Re-enables the per-visit ip-api.com lookup that Cloudflare's country header already answers. |
| `PAGE_DEFAULT_TIER` | Harmless today — `PAGE_DEFAULT_VISIBILITY` is set and is an accepted alias — but the gate's flip lever should be the canonical name. |
| `LLMS_SMALL_TIER` / `LLMS_FULL_TIER` | Harmless (`run.py` defaults both to `public` explicitly), but the knob is invisible on the dashboard. |
| `WEB_CONCURRENCY` | gunicorn falls back to its own worker default. |

And carrying two variables **nothing reads any more** — Gen-1 leftovers retired
with the single-module tracker; delete them so the dashboard stops implying
they do something:

- `SATELLITE_ANALYTICS_DRY_RUN`
- `SATELLITE_APP_ID` (the trio reads `SATELLITE_APP_KEY`)

`PYTHONUNBUFFERED` belongs in the **Dockerfile**, not the dashboard — it is set
there already (`ENV PYTHONUNBUFFERED=1`), which is why it is correctly absent
from the service's variable list.

### Registering the satellite on the primary — two separate lists

Easy to conflate, and each fails differently.
Expand Down Expand Up @@ -429,8 +505,10 @@ offline. The hub verifies the Clerk token against Clerk's JWKS and pins
the environment table.
2. `GET /llms.txt`, `/robots.txt`, `/sitemap.xml` all 200, and the sitemap URLs
use `leaflet.2plot.dev` (i.e. `APP_BASE_URL` is set correctly).
3. Sign in from the site — you should bounce to 2plot.ai and land **back here**,
not on the primary's home page.
3. Sign in from the site — you should bounce to **`2plot.ai/onboarding`** (not
`accounts.2plot.ai`) and land **back here**, not on the primary's home page.
Landing on the primary means `CLERK_SATELLITE_SIGN_IN_REDIRECT` is unset;
the boot log warns about it, and "The sign-in return trip" above is the fix.
4. `/admin/control-board` shows the page table with **no** dev-mode banner.
4b. The gate's boot line reads `access wiring ON` (see "Shipping dark, then
flipping"). With `PAGE_DEFAULT_TIER=public` that is the dark launch:
Expand Down
58 changes: 50 additions & 8 deletions lib/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,29 @@
CLERK_SATELLITE_DOMAIN "leaflet.2plot.dev" (host only, no scheme)
CLERK_SIGN_UP_URL https://2plot.ai/sign-up
CLERK_SATELLITE_SIGN_IN_REDIRECT
OPTIONAL (dash-clerk-auth >= 0.9.2). An absolute URL
on the PRIMARY that the Sign In button navigates to,
with this page carried in ?returnTo=. Read by the
package itself, so we do not pass it through. Unset,
sign-in falls back to Clerk.redirectToSignIn() with
this page forced as the return — the behaviour this
site already ships. Only set it once 2plot.ai has a
page that honours ?returnTo=.
REQUIRED in production, despite reading as optional.
An absolute URL on the PRIMARY that the Sign In
button navigates to, with this page carried in
?returnTo=. Read by dash-clerk-auth from the
environment itself, so we do not pass it through.
Set to https://2plot.ai/onboarding (render.yaml).

It was left unset here while the note below said
"only set it once 2plot.ai has a page that honours
?returnTo=". 2plot.ai gained exactly that page —
/onboarding, which validates returnTo against its
own satellite whitelist and force-redirects home —
and nobody came back to set it. The result, live on
2026-08-20: sign-in succeeded on the primary and
stranded the user there.

Unset, the fallback is Clerk.redirectToSignIn(),
which hops to CLERK_SIGN_IN_URL — the Clerk-HOSTED
Account Portal. The hub's Dash app never sees that
request, so none of its returnTo handling runs and
the return depends solely on the Clerk dashboard's
own allowed-redirect list. That failure is silent
on both sides. See the boot warning in register().

Admin allowlist (drives /admin/control-board):
ADMIN_EMAILS comma-separated, case-insensitive
Expand Down Expand Up @@ -263,6 +278,33 @@ def register() -> bool:
f"CLERK_SATELLITE_DOMAIN={SATELLITE_HOST} (and CLERK_IS_SATELLITE=true) "
"or ClerkJS will fail with 'satellite needs a domain'."
)
sat_redirect = (os.getenv("CLERK_SATELLITE_SIGN_IN_REDIRECT") or "").strip()
if is_satellite and not sat_redirect:
# The one Clerk misconfiguration with NO error on either side: the
# user signs in successfully and is simply left on the primary. It
# shipped that way here for weeks because nothing said so. Now
# something does.
print(
"[auth] ⚠️ Satellite mode with CLERK_SATELLITE_SIGN_IN_REDIRECT "
"unset — sign-in will hop to the Clerk Account Portal instead of "
"the hub, and users may not be returned to this site at all. "
"Set it to https://2plot.ai/onboarding (see render.yaml)."
)
elif is_satellite and not sat_redirect.startswith(("http://", "https://")):
# It is a URL, not a flag, and the package does not reject a bad one:
# it logs a warning and uses the value anyway. `buildSatelliteRedirect`
# concatenates `<value>?returnTo=<here>`, so a non-absolute value is
# resolved against THIS host — CLERK_SATELLITE_SIGN_IN_REDIRECT=true
# sends the Sign In button to https://<this host>/true?returnTo=...,
# which is strictly worse than leaving it unset. The package's own
# warning goes to `logger`, several screens up in the boot output;
# this one prints where the rest of the [auth] diagnostics are.
print(
f"[auth] ⚠️ CLERK_SATELLITE_SIGN_IN_REDIRECT={sat_redirect!r} is not "
"an absolute http(s) URL. It is a DESTINATION, not a flag — the "
"Sign In button will navigate to a path on THIS host and 404. "
"Set it to https://2plot.ai/onboarding."
)

if is_satellite and sat_domain:
_install_satellite_signin_delegation()
Expand Down
33 changes: 33 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,39 @@ services:
value: https://accounts.2plot.ai/sign-in
- key: CLERK_SIGN_UP_URL
value: https://accounts.2plot.ai/sign-up
# THE RETURN TRIP. Without this, sign-in reaches the primary and the
# user never comes back — the exact failure observed on this host
# 2026-08-20.
#
# Two different paths exist, and only one of them is ours:
#
# SET (this): the Sign In button NAVIGATES to
# https://2plot.ai/onboarding?returnTo=<this page>.
# That is a page on the hub's own Dash app, which
# injects a script that validates returnTo against
# lib.auth.allowed_redirect_origins() (leaflet.2plot.dev
# is in that list) and then force-redirects home. The
# network owns and can debug every hop.
#
# UNSET: dash-clerk-auth falls back to
# Clerk.redirectToSignIn(), which goes to
# CLERK_SIGN_IN_URL — accounts.2plot.ai, Clerk's HOSTED
# Account Portal. The hub's Dash app is never in the
# loop, so none of its returnTo machinery runs, and the
# return depends entirely on the Clerk dashboard's own
# allowed-redirect list. When that does not name this
# host, ClerkJS silently drops signInForceRedirectUrl
# and uses the portal's default after-sign-in URL: the
# user authenticates successfully and lands on 2plot.ai.
# No error, client-side or server-side.
#
# dash-clerk-auth reads this name from the environment itself
# (satellite_sign_in_redirect's env fallback), so lib/auth.py does not
# pass it through. `assets/auth_gate.js` appends &mode=signup for the
# "Create free account" button, which /onboarding reads to open the
# sign-UP modal instead — so this one value serves both buttons.
- key: CLERK_SATELLITE_SIGN_IN_REDIRECT
value: https://2plot.ai/onboarding
# REQUIRED in satellite mode. A production custom-domain instance cannot
# derive this from CLERK_SIGN_IN_URL — it must be set explicitly.
- key: CLERK_FRONTEND_API
Expand Down
Loading