# Service principals

> Non-human organization members that can't sign in, hold a role through a membership, and run organization-owned workflows and service webhook endpoints.

Source: https://bettersupabase.com/docs/blocks/service-accounts

`service-accounts` gives an organization members that aren't people: an
automation account, an integration, a nightly job. Each one is an
`auth.users` row that can't sign in and holds its role through a tenant
membership, so RLS, `member_can` and your authorization provider treat it
like any member with that role, and the audit log names it.

```bash
better-supabase add service-accounts
```

```ts
import { createServiceAccounts } from "better-supabase/blocks/service-accounts";
```

## Create one [#create-one]

`create(organizationId, name, role)` creates the user through the Auth admin
API, then registers it. The user is banned for about 100 years, has no
password, gets an address under `service-accounts.invalid` and carries
`app_metadata.bsb_service_principal`, so nobody can sign in as it. If the
registration is refused, the user is deleted again.

```ts title="app/settings/automations/actions.ts"
"use server";

import {
  createServiceAccounts,
  sqlTransport,
} from "better-supabase/blocks/service-accounts";

const principals = createServiceAccounts({
  transport: sqlTransport(postgres.admin),
  admin: bsb.admin().$authAdmin,
});

export async function addAutomationAccount(organizationId: string) {
  const ctx = await bsb.context();
  return principals
    .create(organizationId, "Automations", "member", {
      default: true,
      createdBy: ctx.auth.claims.sub,
    })
    .orThrow();
}
```

Every function except `list` is service-role only. Pass `createdBy` when a
member asks for the change: that member needs `service_principals.manage`
and must be allowed to assign the role (`can_assign_as`), so nobody hands a
principal more than they hold. Without `createdBy` the service acts on its
own, for setup scripts and migrations.

| Method                           | What it does                                                                 |
| -------------------------------- | ---------------------------------------------------------------------------- |
| `create(org, name, role, opts?)` | Creates and registers a principal; `default: true` makes it the default      |
| `setRole(id, role, opts?)`       | Changes its role and its membership                                          |
| `disable(id, opts?)`             | Removes its membership and its default; the user row stays for the audit log |
| `defaultFor(org)`                | The organization's default principal, or `null`                              |
| `list(org)`                      | The organization's principals, through the `service_principals.read` policy  |

`register_service_principal` refuses a user without the
`bsb_service_principal` flag or without a ban, so an existing account never
becomes a principal.

## What runs as the default principal [#what-runs-as-the-default-principal]

Each organization has at most one default principal. Two features resolve
organization-owned work to it, so you don't need an `automation.runAs` hook
for the common case:

- **Workflow builder.** A version published by the service (owner
  `organization`) runs as the default principal. `workflow_run_principal`
  checks on every step that its role still holds every permission the
  version stored, and refuses the step with `WORKFLOW_PUBLISHER_REVOKED`
  otherwise. Without a `runAs` hook, `versions.runAs` returns
  `{ role: "authenticated", sub, tenant_id, bsb_permission_cap }`, the
  principal capped to the stored permissions. An `automation.runAs` hook
  still overrides it. See [Workflow builder](/docs/blocks/workflow-editor).
- **Outgoing webhooks.** A service endpoint (one without `created_by`) gets
  an event only when the default principal holds the event's read
  permission. Without a default principal, the endpoint's declared
  `permissions` decide, as before. See
  [Outgoing webhooks](/docs/blocks/webhooks-out).

Disabling the default principal stops both: workflow steps fail with "This
organization has no default service principal" and service endpoints fall
back to their declared permissions.

## Your own fields [#your-own-fields]

`options.extraColumns` adds columns to `service_principals`. `create` writes them from `fields`,
and `create` and `list` return them in `fields`. Pass a Standard Schema as `fields`
to type them: writes are validated before they reach the database (a
`validation` error), and reads are parsed. `hooks` refuses, rewrites or
observes any method; see [extending blocks](/docs/extending/blocks).

```ts title="better-supabase.config.ts"
export default defineConfig({
  blocks: {
    "service-accounts": { options: { extraColumns: { purpose: "text" } } },
  },
});
```

```ts
const client = createServiceAccounts({
  transport,
  admin: serviceClient.auth.admin,
  fields: v.object({ purpose: v.optional(v.string()) }),
});
await client.create(organizationId, "Zapier", "member", {
  fields: { purpose: "CRM sync" },
});
```

## Permissions [#permissions]

| Operation                 | Key                         | Default roles |
| ------------------------- | --------------------------- | ------------- |
| `service-accounts.read`   | `service_principals.read`   | admin, owner  |
| `service-accounts.manage` | `service_principals.manage` | admin, owner  |

Under the provider model, the membership row the module writes is what your
provider reads, so a provider that keeps roles elsewhere must give the
principal its role there too. With `blocks.memberships.mode: "custom"` the
module writes no membership at all. If you switch users off through
`blocks.permissions.disabled.user` with an active-row table (such as
`profiles`), add a row for each principal; a user without one is disabled.

## Events [#events]

`service_principal.created`, `service_principal.role_changed` and
`service_principal.disabled` go to the outbox and the audit log under the
`access` category. See [Events](/docs/extending/events).

## SQL [#sql]

| Function                                                                            | Who            |
| ----------------------------------------------------------------------------------- | -------------- |
| `register_service_principal(principal, tenant, name, role, is_default, created_by)` | `service_role` |
| `set_service_principal_role(principal, role, created_by)`                           | `service_role` |
| `disable_service_principal(principal, created_by)`                                  | `service_role` |
| `default_service_principal(tenant)`                                                 | `service_role` |
| `service_principals_list(tenant)`                                                   | members        |