# 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-principals

`service-principals` 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 sql add service-principals
```

```ts
import { createServicePrincipals } from "better-supabase/blocks/service-principals";
```

## 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-principals.invalid` and carries
`app_metadata.bs_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 {
  createServicePrincipals,
  sqlTransport,
} from "better-supabase/blocks/service-principals";

const principals = createServicePrincipals({
  transport: sqlTransport(postgres.admin),
  admin: bs.admin().$client.auth.admin,
});

export async function addAutomationAccount(organizationId: string) {
  const ctx = await bs.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
`bs_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, bs_permission_cap }`, the
  principal capped to the stored permissions. An `automation.runAs` hook
  still overrides it. See [Workflow builder](/docs/blocks/workflow-builder).
* **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.

## Permissions [#permissions]

| Operation                   | Key                         | Default roles |
| --------------------------- | --------------------------- | ------------- |
| `service-principals.read`   | `service_principals.read`   | admin, owner  |
| `service-principals.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 `sql.modules.tenant.mode: "custom"` the
module writes no membership at all. If you switch users off through
`sql.modules.access.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        |