> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polycore.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Firebase Auth

> How a Polycore runner reads and administers your Firebase Auth directory without handing anyone a console seat.

The Firebase Auth integration gives Polycore a native identity surface: look an
account up by email, phone, uid, or federated provider id; page the directory;
read SSO provider configuration; and — behind an explicit write grant and a
human approval — create, update, disable, and delete accounts.

The runner authenticates with Application Default Credentials inside your GCP
environment. Polycore cloud does not receive a service-account key.

## Why this exists

"When did this customer last sign in?" and "why can't this person log in?" are
among the most common support questions there are, and the usual answers are
both bad: give a support engineer a Firebase console seat over the whole
project, or make an engineer stop and look it up.

Auth capabilities make the question answerable in Slack, scoped to reads, and
recorded in the audit log — while account changes still stop for a human.

## Read-only enforcement

<Columns cols={2}>
  <Card title="GCP IAM" icon="badge-check">
    The runner's runtime service account receives `roles/firebaseauth.viewer`.
    GCP rejects write operations at the API boundary.
  </Card>

  <Card title="Runner query surface" icon="shield">
    Built-in Auth queries call read methods only. Mutations are a separate,
    approval-gated family that is never implied by enabling reads.
  </Card>
</Columns>

<Warning>
  A `UserRecord` carries `passwordHash` and `passwordSalt` whenever the service
  account is permitted to read them. The runner returns an **allowlist** of
  fields, so password material never leaves your environment — not to Polycore
  cloud, not into an agent transcript, not into a Slack thread.
</Warning>

## What is deliberately not here

Some Admin SDK methods are absent at every grant level, not merely gated:

| Method                                 | Why it is absent                                                                                                                                                                                                                 |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createCustomToken`                    | Mints a token that authenticates **as** the user. That is an account-takeover primitive with no operational question behind it, and approval is the wrong control for something whose entire effect is handing over an identity. |
| `createSessionCookie`                  | Same, in cookie form.                                                                                                                                                                                                            |
| `verifyIdToken`, `verifySessionCookie` | Take a live credential as input, which would invite tokens into the transcript.                                                                                                                                                  |
| `importUsers`                          | Bulk-writes password hashes; a migration job, not a chat operation.                                                                                                                                                              |
| Provider config writes                 | Changing SSO configuration is a change-managed operation, not an ops-chat one. The **reads** are available.                                                                                                                      |

Password reset and sign-in links *are* available, in the mutation family, for
the reason given below.

## 1. Bind the runtime identity

Grant the runner's service account `roles/firebaseauth.viewer` on the Firebase
project. Add `roles/firebaseauth.admin` only if you intend to enable mutations
with ambient credentials.

## 2. Enable Auth queries

```json polycore.json theme={"system"}
{
  "integrations": {
    "firebase": { "auth": {} }
  }
}
```

One `integrations.firebase` block resolves the project id once and every
service under it shares it — from `FIRESTORE_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT`,
or `GCLOUD_PROJECT`, or from a variable you name with `projectIdEnv`. Auth is an
independent grant: a runner can serve Firestore reads without ever exposing the
auth directory, and the reverse.

## Available capabilities

<ResponseField name="auth.getUser" type="read">
  Reads one account by uid: profile, verification and disabled state, custom
  claims, linked providers, and creation / last sign-in / last refresh times.
</ResponseField>

<ResponseField name="auth.getUserByEmail" type="read">
  Reads one account by exact email address.
</ResponseField>

<ResponseField name="auth.getUserByPhoneNumber" type="read">
  Reads one account by exact phone number in E.164 form.
</ResponseField>

<ResponseField name="auth.getUserByProviderUid" type="read">
  Reads one account by its identifier at a federated provider, e.g. a Google
  account id.
</ResponseField>

<ResponseField name="auth.getUsers" type="read">
  Resolves up to 100 identifiers of mixed kinds — uid, email, phone, provider
  uid — in one call, reporting which matched nothing.
</ResponseField>

<ResponseField name="auth.listUsers" type="read">
  Pages the directory in creation order for enumeration and audit.
</ResponseField>

<ResponseField name="auth.listProviderConfigs" type="read">
  Lists configured SAML or OIDC providers with their endpoints and enabled
  state.
</ResponseField>

<ResponseField name="auth.getProviderConfig" type="read">
  Reads one SAML or OIDC provider configuration by id.
</ResponseField>

### There is no search

Firebase Auth supports exact lookups and a full directory page-through. There is
no substring, prefix, or fuzzy match on email or name, and no amount of
capability design can add one. Each capability says so in its own description,
so a caller without an exact identifier asks for one instead of paging the whole
directory hoping to spot a match.

### Not-found is an answer

Every exact lookup returns `null` rather than raising when nothing matches — the
same shape `firestore.get` uses. A raised error reads like a failed call worth
retrying or routing around; a `null` is the answer to the question.

### No multi-tenancy

Identity Platform tenants are not supported, deliberately. Every capability acts
on the project's own directory.

An optional `tenantId` used to sit on each capability. It was a parameter models
filled in unprompted — one sent `"/"` — and on a project without Identity
Platform *any* value fails inside the Admin SDK, turning a working lookup into an
opaque `Error while parsing response data`. It could only ever break a call that
would otherwise have worked, so it was removed along with the tenant listing
capabilities. If a deployment ever genuinely needs tenants, the scope belongs in
`polycore.json` where it is reviewed once — never in a per-call argument the
model chooses.

## Optional: generic mutations

Mutations are off until you grant them explicitly. Enabling reads never implies
them, and upgrading the runner package never adds them.

```json polycore.json theme={"system"}
{
  "integrations": {
    "firebase": {
      "auth": { "write": { "keyEnv": "AUTH_WRITE_KEY" } }
    }
  }
}
```

* `"ambient"` — writes use Application Default Credentials. Choose this when
  the runtime service account already administers Auth.
* `{ "keyEnv": "…" }` — writes use a **separate** write-capable service account
  read from that environment variable, so the runtime identity stays
  `roles/firebaseauth.viewer`. Only the variable *name* is committed.

Every mutation is `write`-hazard: the control plane holds the request until a
human approves that exact call with those exact arguments.

<ResponseField name="auth.createUser" type="write">
  Creates one account. Fails if the email or phone already belongs to another
  account, so it can never silently take over an existing identity.
</ResponseField>

<ResponseField name="auth.updateUser" type="write">
  Changes email, phone, password, profile, verified flag, or disabled state.
  Only the properties passed change.
</ResponseField>

<ResponseField name="auth.deleteUser" type="write">
  Permanently deletes one account. Removes the identity only — data keyed by
  that uid is untouched, and the uid is unrecoverable.
</ResponseField>

<ResponseField name="auth.deleteUsers" type="write">
  Deletes up to 50 accounts by explicit uid. Every uid appears in the approval.
</ResponseField>

<ResponseField name="auth.setCustomUserClaims" type="write">
  Replaces the user's custom claims — the token fields most apps use for roles
  and entitlements. Replaces rather than merges.
</ResponseField>

<ResponseField name="auth.revokeRefreshTokens" type="write">
  Signs the user out of all devices. The standard response to a compromised
  account, and the way to make a disable or claims change take effect at once.
</ResponseField>

<ResponseField name="auth.generatePasswordResetLink" type="write">
  Generates a password-reset link for an email address.
</ResponseField>

<ResponseField name="auth.generateEmailVerificationLink" type="write">
  Generates an email-verification link for a registered address.
</ResponseField>

<ResponseField name="auth.generateSignInWithEmailLink" type="write">
  Generates a passwordless sign-in link.
</ResponseField>

<Warning>
  The three link capabilities mutate nothing. They are gated because they mint a
  single-use credential that grants control of an account — anyone holding a
  sign-in link is signed in **as** that user. Classifying them as reads because
  they happen not to write would be dishonest about their blast radius. Send
  them to the address they were minted for, never into a shared channel.
</Warning>

### Enumerated targets

Every mutation names its users by uid or email. There is deliberately no
"disable everyone matching this filter": that set would resolve *after*
approval, so nobody could have reviewed what they approved. Bulk work reads the
uids first — no approval needed — and proposes them explicitly.
