You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/rules/sim-url-state.md
+18-3Lines changed: 18 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,7 +34,7 @@ Put state in the URL **only** when it is *all* of: shareable, deep-linkable, boo
34
34
## Anti-patterns (forbidden)
35
35
36
36
- Direct `useSearchParams().get(...)` or `new URLSearchParams(window.location.search)` to **read** state.
37
-
- Hand-built query strings + `router.replace`/`router.push` to **mutate** state.
37
+
- Hand-built query strings + `router.replace`/`router.push` to **mutate** state.**If the target path equals the current path, it is a query mutation, not a navigation** — even when written as a full path template. Re-serializing the path by hand is lossy by construction: it drops every param the template forgets. Use the nuqs setter (`setParams({ key: null }, { history: 'replace', scroll: false })`) — `null` always removes the key, and only the params you name are touched. Both options are already nuqs defaults (see "Conventions"); write them explicitly because a group whose shared options set `history: 'push'` (e.g. `filesUrlKeys`) would otherwise push a back-stack entry for a strip.
38
38
-`window.history.replaceState`/`pushState` to mutate a param.
39
39
- Duplicating URL state into a store and syncing it with effects / `popstate` listeners.
40
40
- High-frequency or large state in the URL (cursor, pan/zoom, un-debounced keystrokes, big JSON blobs).
@@ -44,7 +44,7 @@ These reads/mutations are **not** anti-patterns and stay as-is:
44
44
45
45
-**Outbound URL builders** — `new URLSearchParams({...})` to construct a `href`, a download endpoint, an external WebSocket/API URL, or a `window.open(_, '_blank')` destination.
46
46
-**Route navigations** — `router.push('/path/[id]?folderId=x')` that changes the route *path*, not just the current query. A nuqs setter only mutates the query on the current path; cross-path navigation stays on `router`.
47
-
-**Read-once auth / redirect signals** — `token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `upgraded`, `redirect_workflow`, etc. These are navigation signals consumed once (often read-then-strip), not synced view-state. Leave them on `useSearchParams`.
47
+
-**Read-once auth / redirect signals** — `token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `new` (invite signup flow), `upgraded`, `redirect_workflow`, etc. These are navigation signals consumed once (often read-then-strip), not synced view-state. Leave them on `useSearchParams`. Key names are per-surface: files' `new` is a genuine nuqs param (`files/search-params.ts`), while invite's `new` is a one-shot signup signal.
48
48
49
49
## Per-feature `search-params.ts` — single source of truth
50
50
@@ -128,7 +128,22 @@ If a client param must be re-read server-side after a change, set `shallow: fals
128
128
129
129
## Suspense boundary
130
130
131
-
`useQueryState`/`useQueryStates` read `useSearchParams` internally, so any client component using them must sit under a `<Suspense>` boundary (Next.js requirement). Wrap the page entry with a real-chrome fallback so a suspend never flashes a blank frame — see `apps/sim/app/workspace/[workspaceId]/files/page.tsx`.
131
+
`useQueryState`/`useQueryStates` read `useSearchParams` internally, so any client component using them must sit under a `<Suspense>` boundary (Next.js requirement). Wrap the page entry with a real-chrome fallback so a suspend never flashes a blank frame.
132
+
133
+
**Never `fallback={null}` on a page entry.** The route's co-located `loading.tsx` default export *is* the correct fallback — one skeleton serves both the route-level navigation transition (which Next renders automatically) and the in-page suspend (which this boundary renders). If the segment has no `loading.tsx`, add one; the route transition needs it anyway. Import it absolutely (`sim-imports.md`):
This applies to **page entries**. An inner `<Suspense>` wrapping a `lazy()` component is the exception: there `fallback={null}` is correct, precisely so the suspend resolves at the nearest boundary instead of flashing the whole route — see `sim-imports.md`, "Code-splitting through barrels".
|`DISABLE_REGISTRATION=true`| Blocks all new accounts — email/password, email OTP, and social sign-in. Only existing accounts can sign in, including to accept a workspace invitation. SSO is unaffected|
88
88
|`DISABLE_EMAIL_SIGNUP=true`| Blocks new email/password registrations; existing email login keeps working |
89
89
|`ALLOWED_LOGIN_DOMAINS`| Comma-separated domain allowlist, e.g. `acme.com,acme.co.uk`. Gates email sign-**in** as well as signup |
90
90
|`ALLOWED_LOGIN_EMAILS`| Comma-separated address allowlist, applied the same way |
@@ -93,7 +93,9 @@ See the [SSO guide](/platform/enterprise/sso) for identity-provider setup and th
93
93
|`BLOCKED_EMAIL_MX_HOSTS`| MX-host substrings to block; used only with the above |
94
94
95
95
<Callouttype="warn">
96
-
These controls gate the **email/password** path. A first-time sign-in through Google, GitHub, or Microsoft creates an account through the social provider and is not filtered by them. If you need a hard boundary, disable the social providers you have not vetted (`DISABLE_GOOGLE_AUTH`, `DISABLE_GITHUB_AUTH`, `DISABLE_MICROSOFT_AUTH`) or restrict membership at the identity provider and use SSO.
96
+
`ALLOWED_LOGIN_DOMAINS`, `ALLOWED_LOGIN_EMAILS`, and `SIGNUP_MX_VALIDATION_ENABLED` gate the **email/password** path only. A first-time sign-in through Google, GitHub, or Microsoft creates an account through the social provider and is not filtered by them. To restrict who may sign in through a social provider, disable the ones you have not vetted (`DISABLE_GOOGLE_AUTH`, `DISABLE_GITHUB_AUTH`, `DISABLE_MICROSOFT_AUTH`) or restrict membership at the identity provider and use SSO.
97
+
98
+
`DISABLE_REGISTRATION` and `BLOCKED_SIGNUP_DOMAINS` apply to every path, social included.
97
99
</Callout>
98
100
99
101
For a company deployment, the usual pairing is domain-restricted signup plus SSO:
Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -107,7 +107,7 @@ See [Authentication](/platform/self-hosting/authentication).
107
107
108
108
| Variable | Description |
109
109
|----------|-------------|
110
-
|`DISABLE_REGISTRATION`| Set `true` to disable new user signups entirely|
110
+
|`DISABLE_REGISTRATION`| Set `true` to block all new accounts, including social sign-in. Invitations still work for people who already have an account. SSO is unaffected|
111
111
|`DISABLE_EMAIL_SIGNUP`| Block new email/password registrations; existing email login keeps working |
112
112
|`ALLOWED_LOGIN_DOMAINS`| Restrict signups to domains (comma-separated) |
113
113
|`ALLOWED_LOGIN_EMAILS`| Restrict signups to specific emails (comma-separated) |
0 commit comments