{
  "name": "app-users",
  "title": "App users - sign-in owned by your app",
  "description": "App-owned users (gipity project auth app): people sign in to one app with Google, Discord, Apple, a 6-digit email code, Steam or a guest device, without a Gipity account. Sessions, account linking, your own OAuth credentials, user_created / user_deleted hooks, managing and exporting users",
  "guid": "sk_plat_ausr",
  "category": "App services",
  "requiredTools": [],
  "content": "# App users - sign-in owned by your app\n\nAn app in `app` mode (`gipity project auth app`, or `both` to also offer Sign in with Gipity) has its own users: people sign in to **this app only**, with no Gipity account and no consent screen, and the app owns them. Use it for anything you put in front of the public; use Sign in with Gipity for your own tools and for AI apps where users pay for their AI use (see [app-auth](app-auth.md)).\n\n## Using it\n\nTurn it on in gipity.yaml (a top-level block, applied by every deploy):\n\n```yaml\nauth:\n  mode: app        # or both, to offer Sign in with Gipity too\n```\n\n(`gipity project auth app` sets it at once and writes that block.) Then the same client code works, with no new endpoints to call; `Gipity.auth.signInAsGuest()` signs a browser in as its guest with no prompt (for \"no sign-in\" apps):\n\n```js\nconst user = await Gipity.auth.signIn();   // opens the app's own sign-in page in a popup\n// user: { guid, displayName, avatarUrl, email, provider }   (provider: 'google' | 'discord' | 'apple' | 'email')\nawait Gipity.auth.user();                  // the signed-in user, or null\nawait Gipity.auth.signOut();               // this browser\nawait Gipity.auth.signOutEverywhere();     // every device\n```\n\nThe sign-in page is branded as your app (name, logo and accent color from `gipity brand` / the `auth_branding:` deploy phase) and offers each provider that is set up plus an email code. A signed-in browser holds an httpOnly session cookie for your app only (30 days, renewed on use, revocable): page JavaScript never sees a token, and another app's code can't use it.\n\n**In functions** an app user is an ordinary signed-in user (`ctx.auth.userGuid`, `displayName`), plus `ctx.auth.identity = { provider, id, email }`. `email` is their verified address (null for Steam and guest players), so the app can email its own users with the `email()` service. Gipity-account callers have `identity: null` and no email.\n\n**One person is one user per app.** A provider seen before signs in to its user; a new provider whose verified email matches an existing user links to that user (sign in with Google today, an email code tomorrow: same `userGuid`). An unverified email never links. The same person in two different apps is two separate users.\n\n**Providers and credentials.** `gipity project auth` shows each provider's state.\n- **Google, Discord:** work right away on Gipity's shared credentials; the provider's screen says \"Gipity Apps\". Fine while you build. Before the app goes public on a custom domain, set your own so the screen shows your app: `gipity project auth provider google` prints the redirect URI and steps, then `gipity project auth provider google --client-id <id> --client-secret <secret>`. Existing users carry over (Google ids are the same across credentials). `gipity project auth` warns when an app with a custom domain still uses shared credentials.\n- **Apple:** needs your own credentials from day one (`gipity project auth provider apple`), because Apple ties user ids to the developer team.\n- **Email codes:** always on in app mode; sent branded as your app, rate-limited per address.\n\n**Lifecycle hooks.** Declare functions with `hooks:` in `gipity.yaml` (use `auth: member` so only the platform and you can run them):\n- `user_created`: runs after a new user signs up, with `ctx.trigger = { type: 'user_created', userGuid, user: { provider, displayName, email } }`. Use it for a welcome email or a profile row. Sign-up never waits on it; failures show in `gipity logs fn <name>`.\n- `user_deleted`: runs before a user is erased (they deleted their account with `DELETE /api/<PROJECT_GUID>/auth/player`, or you did); delete their rows. A failure stops the deletion with 502 `PLAYER_CLEANUP_FAILED`.\n\n**Managing users** (owner): `gipity project users` lists and searches them, `gipity project users export --format csv -o users.csv` exports all, `gipity project users delete <guid>` erases one (running `user_deleted` hooks), and `gipity project users signout <guid>` ends all of their sessions.\n\n**Native clients** (games, mobile, desktop) don't use the page or the cookie: they sign in over HTTPS and send the returned token as `Authorization: Bearer`. Steam and guest devices: see [steam-game](steam-game.md). Email codes: `POST /api/<PROJECT_GUID>/auth/email/start { email }`, then `POST /api/<PROJECT_GUID>/auth/email/verify { email, code }`, which returns `{ data: { token, expiresIn, user } }`.\n\n| Endpoint | What |\n|---|---|\n| `GET /api/<PROJECT_GUID>/auth/player` | The signed-in app user (`guid`, `displayName`, `email`, `provider`, `providerUserId`) |\n| `PATCH /api/<PROJECT_GUID>/auth/player` | Set or clear their display name (`{ displayName }`); `gipity project auth --unique-names on` makes names unique |\n| `DELETE /api/<PROJECT_GUID>/auth/player` | Erase their account (runs `user_deleted` hooks) |\n| `POST /api/<PROJECT_GUID>/auth/signout[?everywhere=1]` | End the browser session (send `X-Gipity-Logout: 1`); the SDK's `signOut()` does this |\n\n**Limits.** App users have no Gipity credits, so `user_pays` services can't bill them; those calls are billed to you (owner pays). There are no passwords, MFA or organizations; if an app needs those, it needs a dedicated identity provider.\n\n"
}
