Native organization switching
ENG-2751 moves the signed-in sidebar
switch to POST /session/organization, taking { organization_id: slug } and
returning 204 with a renewed cookie. It never provisions accounts or memberships.
Hosted/headless login, explicit login links, invite acceptance and identity
schedules remain retained follow-ups.
Contract and concurrency
- An exact non-null allowed Origin and a valid persisted Rack/Devise session are required. Bearer tokens cannot use this endpoint. Existing expiry, staging idle timeout and password-salt checks apply even on a cache hit.
- Select a kept membership belonging to this account and requested slug. Require the target role to be global or owned by that organization. The WorkOS refresh response must match both stored provider user ID and target organization ID.
- Hold the session row lock before using its rotating refresh token; a second switch waits and then rejects the old ID. Identity locks fence password and membership changes. Revalidate the salt after locking the identity. Each WorkOS request has a three-second timeout and no automatic retries; pending status reconciliation may make a second bounded request. PostgreSQL lock acquisition has a three-second timeout. A failure rolls back local writes. WorkOS token rotation cannot be rolled back with PostgreSQL: a provider success followed by a lost response or local failure can require signing in again. Do not automatically retry the exchange or claim distributed atomicity.
- A logout that committed first leaves no session to rotate. Once rotation has committed, logout uses the replacement cookie. Already-authorized in-flight requests are not retroactively cancelled. Tests must not claim otherwise.
- Preserve local role/scopes, pending membership status healing and blank-name employee healing. Only the owning account’s linked placeholder employees are changed; named/unrelated employees remain intact.
- Replace refresh/sealed credentials and tenant selection atomically. Change the public Rack cookie ID when the organization changes, remove old representations, and preserve CSRF/unrelated Marshal keys. Rails renewal resets creation time; selecting the same organization keeps it. Production uses the verified 30-day shared HTTP-only/Secure/SameSite=None cookie; staging keeps its 12-hour lifetime.
- Seal the WorkOS payload using Ruby’s AES-256-GCM envelope so Rails rollback still supports session refresh/logout. Node’s default sealed-session format is incompatible and must not be substituted.
- The UI submits only on selection, disables repeated submissions, clears
persisted/in-memory queries, then fully reloads
/. Errors retain the prior tenant. A 401 uses the existing sign-in redirect.
Evidence and rollout
64 focused API2 tests and five UI tests pass. An isolated PostgreSQL 15.10 rehearsal proves switch/switch contention, logout-first denial and transaction rollback. Installed WorkOS Ruby 6.2.0 unseals a native response. Temporary local database cleanup is verified. The protocol follows the WorkOS refresh contract .
Both regions’ WorkOS settings/cookie scope matched Rails at September 3 10:40; fresh runtime configuration was rechecked in the 11:00 run. Deployed Rails cookie expiry is 43,200 minutes, retention 720 hours, idle timeout absent. Before merge require package typechecks/lint, CI and review. After automatic deployment require actual regional switch, old-cookie rejection, target-role access and replacement-cookie logout evidence plus 24 hours without regression. No synthetic production account/session, replay or secret mutation is authorized.
Observe native_organization_switched and native_organization_switch_failed,
401/403/503 responses, WorkOS timeouts and session lock waits. Logs contain no
cookies, tokens, upstream error bodies or serialized session data. Production
now ships runtime code under /app/dist; source-only ad hoc probes must not
assume the former /app/rulebase-api2/src path still exists.
Rollback the sidebar import/action to its preceding Rails login flow first. Keep the shared session format and existing Rails routes until all AUTH flows are transferred. No schema change or persistent runtime flag is introduced.