Skip to content
SLICES / TECHNICAL FOLIO Nº 001FULL-STACK ASSEMBLIES — OPEN SOURCE

Headless / Organizations

Domain access

List members, edit roles, manage domain access and pending invitations.

11 public parts · 1examples

@slices.sh/auth/headlessView as Markdown ↗

Your design. Our behavior.

Explore the tree to see what each part controls. Gates reveal the relevant state; roots outline their scope.

Behavior / inputGateGenericRenderer · not headlessScope
COMPONENT ANATOMYHover · focus · tap
ONE POSSIBLE DESIGNStatic preview

User.Root · Scope

User.RootSays which auth the User parts below read, and whether they may do without it. The parts need no root: left out, they read the nearest auth root's config, or "default", and render nothing without auth. `<User.Root enabled="optional">` is for the one who wants that said in the code.

Installation

Choose what your component needs. Run these commands in an initialized Next.js app.

0

Before you start

The steps below run in a Next.js app with shadcn set up (it has a components.json). In an existing Next.js app, such as one from create-next-app, set shadcn up first:

npx shadcn@latest init -t next -b base -p nova

Or start fresh: the slices CLI creates the app and sets it up.

npx slices.sh install
1

Install the service registry

Adds services/index.ts, which import "@/services" loads. A provider setup writes the same file, so skip this if your app has one.

npx add services
2

Install the headless library

npm

Add the React parts to your app, then compose them with your own elements.

npm install @slices.sh/auth@^0.1.10

Import from @slices.sh/auth/headless.

3

Install the service

Connect the parts to a provider. Skip this step if this service is already configured in your app.

Provider
Database
Authentication methods
Account capabilities
npx add auth.betterauth --auth-methods google --organizations

Adds service configuration and API routes to your app, with the provider dependencies. For a headless composition, add import "@/services" after "use client".

The slices CLI installs the service and provisions its required resources in one command.

Set BETTER_AUTH_URL to your app origin. Memory is for local development; use a persistent database for deployment. Neon and Supabase also require the migration script printed by the installer.

Provider setup notes
  • BETTER_AUTH_SECRET signs every session and is generated into .env.local while this installs (32 random bytes, base64, nothing leaves the machine). Changing it signs everyone out; in production set the same one on every instance.
  • The public origin (OAuth callbacks, the passkey domain) is the one each request comes to in development, and the deployment's own on Vercel. Elsewhere, or behind a proxy or a custom domain, set BETTER_AUTH_URL, such as https://acme.com.

Start small

Parts bring the behavior. Use their default elements, or supply your own with render.

example.tsx
"use client"import "@/services"import { User, Members } from "@slices.sh/auth/headless"export function Example() {  return (    <User.Root>      <ul>        <Members.Item>          <Members.Name />          <Members.Email />        </Members.Item>      </ul>      <Members.Empty>No members.</Members.Empty>    </User.Root>  )}

Requires an authenticated account. Organization controls also require organizations and the appropriate role.

API reference

Every public part used by this experience is listed below. Gates render children without adding an element. Element parts accept native element props and render; function children receive the state declared in their props. Scopes provide context rather than a visual element.

Members.AccessScopescope

An organization/role change cannot leave a now-hidden access tab selected.

Props

1 documented props

Members.AccessScope props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.AccessScope from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/access-scope.tsx

Members.DomainAccessgate

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Props

1 documented props

Members.DomainAccess props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainAccess from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-access.tsx

Members.NoDomainAccessgate

Why nobody can claim a domain here yet. The mirror of `Members.DomainAccess`, and not a courtesy: claiming a domain has four conditions and meeting none of them looks exactly like meeting three, so a screen that only drew the form drew nothing and read as a feature that does not work. Each reason is a different next step — sign in, verify the address, make an organization, ask an owner — and words for them belong to the pattern. → docs/rules/capability-honesty.md Closed while the session and the organization are still loading, so a page does not accuse somebody of being signed out for the half-second before it knows.

Capability: organizations

Props

1 documented props

Members.NoDomainAccess props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { blocked: DomainBlock }) => ReactNode)—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.NoDomainAccess from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/no-domain-access.tsx

Members.DomainErrorcontent · <p>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Props

2 documented props · Native <p> attributes supported

Members.DomainError props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { message: string }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainError from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-error.tsx

Members.DomainRoleinput · <select>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Required scope

Members.DomainRole must be inside <Members.AddDomain>.

Props

2 documented props · Native <select> attributes supported

Members.DomainRole props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { roles: readonly ("member" | "admin")[] }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainRole from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-role.tsx

Members.RemoveDomainaction · <button>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Required scope

Members.RemoveDomain must be inside <Members.Domains>.

Props

2 documented props · Native <button> attributes supported

Members.RemoveDomain props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.RemoveDomain from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/remove-domain.tsx

Members.DomainSubmitaction · <button>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Required scope

Members.DomainSubmit must be inside <Members.AddDomain>.

Props

2 documented props · Native <button> attributes supported

Members.DomainSubmit props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainSubmit from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-submit.tsx

Members.DomainInputinput · <input>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Required scope

Members.DomainInput must be inside <Members.AddDomain>.

Props

2 documented props · Native <input> attributes supported

Members.DomainInput props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { invalid: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainInput from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-input.tsx

Members.DomainSummarycontent · <p>

What allowing this domain will do, in the words of the domain in the field. A form headed "Invite by domain" said nothing about what the domain was for, and the only sentence anywhere near it was the one refusing a free provider's — which is how a screen ends up explaining that gmail.com is nobody's before saying what a domain does. The sentence follows what is typed, so it is the field's own answer rather than a note.

Capability: organizations

Required scope

Members.DomainSummary must be inside <Members.AddDomain>.

Props

2 documented props · Native <p> attributes supported

Members.DomainSummary props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { domain: string; organization: string | null }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.DomainSummary from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domain-summary.tsx

Members.AddDomainaction · <form>

The form that allows a whole domain into this organization. Drawn only where every condition of claiming is met. It used to be drawn to everybody — the access object it checked is never falsy — so a member, or somebody with no organization open, got a form whose only possible answer was a refusal about permission. That is what read as a feature that does not work. → docs/rules/capability-honesty.md

Capability: organizations

Props

2 documented props · Native <form> attributes supported

Members.AddDomain props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean; error: string | null }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.AddDomain from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/add-domain.tsx

Members.Domainscontent · <div>

Letting a whole private domain join. DomainAccess opens where every condition of claiming is met, and NoDomainAccess says which one is not, as a different next step each. AddDomain is the form, with DomainInput (opening on the person's own domain), DomainRole, DomainSummary (what allowing it will do), DomainSubmit and DomainError. Domains repeats the claims and RemoveDomain removes one. Public mail domains are refused.

Capability: organizations

Required scope

Members.Domains renders its own scope.

Props

2 documented props · Native <div> attributes supported

Members.Domains props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { domain: string; role: string }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Members.Domains from @slices.sh/auth/headless/members

Source: @slices.sh/auth/src/headless/members/domains.tsx

User.Rootscope

Says which auth the User parts below read, and whether they may do without it. The parts need no root: left out, they read the nearest auth root's config, or "default", and render nothing without auth. `<User.Root enabled="optional">` is for the one who wants that said in the code.

Props

3 documented props

User.Root props
PropTypeDefault
config

The auth config, a slug from the registry (services/auth/<config>.config.ts). The nearest root's, else "default", when left out.

string—
enabled

Left out or true: required, and missing auth throws. "optional": renders its children and every part inside degrades without auth. false: off. A capability name such as "methods: password": requires auth with it.

Enabled—
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • User.Root from @slices.sh/auth/headless/user
  • User.Root from @slices.sh/auth/headless/passkeys
  • User.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/root.tsx

Organization.Activegate

Item is a <button> repeated once per organization with { active, pending }; clicking makes it active. Personal switches to the personal account. Active renders its children with the active organization in scope, and nothing on the personal account; None is its mirror, once the answer is in. Switcher holds the searchable list: Switcher.Surface maps onto a Popover or a Sheet, Switcher.Options, Input, List, Item, Empty and Error search and list, and Switcher.Shortcuts binds keys to switch.

Capability: organizations

Props

1 documented props

Organization.Active props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • Organization.Active from @slices.sh/auth/headless/organization

Source: @slices.sh/auth/src/headless/organization/active.tsx

When something is missing

No service configuration
Install the auth setup below and import "@/services". Required service roots report missing setup; gates do not replace that configuration.
Missing capability or parent scope
Follow each part’s context requirements and the example composition. Capability-aware controls hide when the provider cannot perform their action.
Read the setup guide →