Identity and Privacy
What Shoo stores, what your app receives, and where identity boundaries are
Read this page before you create user records keyed on Shoo IDs.
The origin is the app
Shoo has no app registration. Your app's identity is the origin of its callback URL: scheme, host, and port.
client_id = audience = "origin:" + new URL(redirect_uri).originEach of these is a different app to Shoo:
| Origin | Audience |
|---|---|
https://app.example.com | origin:https://app.example.com |
https://www.app.example.com | origin:https://www.app.example.com |
http://localhost:5173 | origin:http://localhost:5173 |
https://my-app-git-pr-12.vercel.app | origin:https://my-app-git-pr-12.vercel.app |
A different origin gets a different audience and a different pairwise_sub for the same Google account.
What this means for you:
- Pick your production origin before launch. Users who sign in on
wwwand on the bare domain become two different users. - Previews and localhost are separate apps. Their users do not match production users. Never accept a preview or localhost audience in your production backend.
- Pin one exact audience per backend. Configure it per environment, for example with
APP_ORIGIN. Do not derive it from the requestHostheader in production. - Shoo does not migrate domains. If you move to a new origin, your users get new IDs. Shoo does not merge accounts by email. To migrate, link the old and new IDs in your own database, for example by asking each user to sign in on both origins in one session. Do not link accounts by email alone.
Origins other than https: are accepted only for localhost, 127.0.0.1, and ::1.
Pairwise subject
pairwise_sub (also in sub) is the user ID your app should store.
- It is the same every time the same Shoo user signs in to the same origin.
- It is different for every origin.
- It is opaque. It is not the Google account ID and it does not contain one.
- Shoo creates it the first time the user signs in to your origin and stores it. Disconnecting and reconnecting the app keeps the same value.
- If a user deletes their Shoo account and signs up again, they become a new Shoo user and get a new
pairwise_sub.
The format is ps_ followed by a hex string. Treat it as an opaque string.
What your app receives
Every token contains:
| Claim | Value |
|---|---|
iss | https://shoo.dev |
aud | origin:<your-origin> |
sub, pairwise_sub | The pairwise subject |
iat, exp | Issue and expiry time |
jti | A unique token ID. It does not prevent replay. |
Only when your app requests profile data and the user agrees:
| Claim | Value |
|---|---|
email | Google account email |
email_verified | Google's email verification flag |
name | Google display name |
picture | Google profile picture URL |
pii_sub | A Shoo user ID that is the same for every app |
pii_sub links users across apps
pii_sub is a legacy claim. It is kept so existing apps continue to work.
It is the same value for one user in every app that received profile consent. Two apps that both store pii_sub can tell that they have the same user. The profile consent screen tells the user this before they agree.
Use pairwise_sub for your user records. Use pii_sub only if you run several origins and need to know that a user is the same person across them.
Pairwise subjects do not make a user anonymous. Apps that receive the same email address can also link the user.
What Shoo stores
| Data | Why |
|---|---|
Google account ID (sub) | To recognize the user |
| Profile fields returned by Google | Only when profile access was requested |
A random pii_sub per user | Legacy cross-app ID |
One pairwise_sub per user and origin | Stable per-app ID |
| Per-origin approval and profile consent | To remember what the user allowed |
| Short-lived codes, tickets, rate-limit counters, IP addresses | Security and abuse prevention |
A signed session cookie on shoo.dev | Keeps the user signed in to Shoo |
| After account deletion: a deletion record | See account deletion |
On HTTPS the session cookie is __Host-shoo_session. A local HTTP development server uses shoo_session.
See the privacy policy.
User controls
Users manage Shoo at https://shoo.dev/me. Each control has a different effect. See tokens and sessions.
Account deletion
Deleting a Shoo account deletes the user's account, profile data, pii_sub, pairwise_sub values, app approvals, profile consents, and pending codes. Short-lived security rows, such as rate-limit counters, expire on their normal schedule.
Shoo keeps one deletion record per deleted Google account:
| Field | Value |
|---|---|
| Identity hash | SHA-256 of a fixed prefix and the Google account ID (sub) |
| Deletion time | When the deletion happened |
| Deletion checkpoint | An internal sequence number that orders auth events |
The record lets Shoo reject sign-ins that started before the deletion. It also lets Shoo match the account if a backup from before the deletion is restored. The hash has no secret key, so anyone who knows the Google account ID can match it to the record.
Shoo does not delete deletion records automatically today. A retention period is not set yet.
If the user signs in again later, Shoo creates a new account with new pairwise_sub values.
Deleting a Shoo account does not delete your app's records. If you store user data, give users a way to delete it in your app.