Skip to main content
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

GCP IAM

The runner’s runtime service account receives roles/firebaseauth.viewer. GCP rejects write operations at the API boundary.

Runner query surface

Built-in Auth queries call read methods only. Mutations are a separate, approval-gated family that is never implied by enabling reads.
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.

What is deliberately not here

Some Admin SDK methods are absent at every grant level, not merely gated: 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

polycore.json
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

read
Reads one account by uid: profile, verification and disabled state, custom claims, linked providers, and creation / last sign-in / last refresh times.
read
Reads one account by exact email address.
read
Reads one account by exact phone number in E.164 form.
read
Reads one account by its identifier at a federated provider, e.g. a Google account id.
read
Resolves up to 100 identifiers of mixed kinds — uid, email, phone, provider uid — in one call, reporting which matched nothing.
read
Pages the directory in creation order for enumeration and audit.
read
Lists configured SAML or OIDC providers with their endpoints and enabled state.
read
Reads one SAML or OIDC provider configuration by id.
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.
polycore.json
  • "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.
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.
write
Changes email, phone, password, profile, verified flag, or disabled state. Only the properties passed change.
write
Permanently deletes one account. Removes the identity only — data keyed by that uid is untouched, and the uid is unrecoverable.
write
Deletes up to 50 accounts by explicit uid. Every uid appears in the approval.
write
Replaces the user’s custom claims — the token fields most apps use for roles and entitlements. Replaces rather than merges.
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.
Generates a password-reset link for an email address.
Generates an email-verification link for a registered address.
Generates a passwordless sign-in link.
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.

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.