All guides

Authentication and Roles

Set up login for CMS editors, manage team members, assign roles, and invite collaborators.

3 min read499 words

Airdraft has a built-in authentication system for CMS editors — distinct from your public website's auth. This guide covers setting up login, managing team members, and understanding roles.

Roles

Role What they can do
admin Full access — create/edit/delete entries in any collection, manage users, edit schema, manage media, view audit log
publisher Create, edit, and publish entries; manage media; cannot manage users or edit schema
editor Create and edit entries (draft only); cannot publish, manage users, or edit schema

Note: On Airdraft Cloud, the team creator is the first admin. There is no separate "owner" role.


Airdraft Cloud — team management

In the dashboard, go to Settings → Team.

Inviting a team member

  1. Click Invite member.
  2. Enter the person's email address.
  3. Select a role (admin, publisher, or editor).
  4. Click Send invite.

The invitee receives an email with a sign-up link. Once they accept, they appear in the team list.

Changing a role

  1. Find the member in the Members list.
  2. Click the role badge next to their name.
  3. Select the new role.

Changes take effect immediately.

Removing a member

  1. Find the member in the Members list.
  2. Click ⋯ → Remove from team.
  3. Confirm.

The removed member loses access immediately.


Self-hosted — authentication setup

When self-hosting, add the @airdraft/plugin-auth plugin. The CLI adds this automatically if you select "Authentication" during npx airdraft init.

Credentials (email + password)

// airdraft.config.ts
import { defineConfig, LocalAdapter } from '@airdraft/core'
import { withAutoAuth } from '@airdraft/plugin-auth'

const adapter = new LocalAdapter({ root: './content' })
const auth = withAutoAuth()  // reads AIRDRAFT_JWT_SECRET from env; returns null if not set

export default defineConfig({
  adapter,
  schemaPath: 'airdraft.schema.json',
  plugins: [
    ...(auth ? [auth] : []),
  ],
})

This adds all auth routes at /api/cms/auth/*. Users are stored in .airdraft/users.json by default.

Route Method Description
/auth/login POST Email + password login
/auth/logout POST Invalidate session
/auth/refresh POST Rotate access token
/auth/me GET Return current user
/auth/users GET List all users (admin only)
/auth/users/:id/role PATCH Change a user's role (admin only)
/auth/users/:id DELETE Remove a user (admin only)
/auth/invites GET List pending invites (admin only)
/auth/invites POST Send an invite (admin only)
/auth/invites/:token DELETE Revoke an invite (admin only)
/auth/invites/accept POST Accept an invite and set password

withAuth (explicit provider)

Use withAuth when you need to explicitly configure a provider or restrict public paths:

import { withAuth, CredentialsProvider, UserStore } from '@airdraft/plugin-auth'

const userStore = new UserStore()  // reads/writes .airdraft/users.json

const auth = withAuth({
  provider: CredentialsProvider({
    userStore,
    secret: process.env.AIRDRAFT_JWT_SECRET!,
    roles: { 'admin@example.com': 'admin' },  // optional per-email role overrides
  }),
  publicPaths: ['/api/cms/posts'],  // bypass auth on these routes
})

Creating the first admin

After setup, create the first admin user:

npx airdraft create-user

The CLI prompts for email, password, and role.

OAuth providers

Add GitHub or Google OAuth by including the provider in the plugin config:

import { withAuth, CredentialsProvider, GitHubOAuthProvider } from '@airdraft/plugin-auth'

const auth = withAuth({
  provider: CredentialsProvider({ userStore, secret: process.env.AIRDRAFT_JWT_SECRET! }),
})

// Or use a GitHub OAuth provider:
const auth = withAuth({
  provider: GitHubOAuthProvider({
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    secret: process.env.AIRDRAFT_JWT_SECRET!,
    allowedOrgs: ['my-org'],   // optional: restrict by org
    allowedUsers: ['my-user'], // optional: restrict by username
    defaultRole: 'editor',
  }),
})

// Or Google:
const auth = withAuth({
  provider: GoogleOAuthProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    secret: process.env.AIRDRAFT_JWT_SECRET!,
    allowedDomains: ['mycompany.com'],
    defaultRole: 'editor',
  }),
})

OAuth routes are added automatically at /auth/github + /auth/github/callback (or /auth/google + /auth/google/callback).


Protecting CMS routes

By default, all write operations (POST, PUT, DELETE, PATCH) require a valid session or API key. Read operations on published content can be configured to be public.

To require authentication for all reads:

withAuth({
  // ...
  requireAuthForReads: true,
})

To allow public reads of published content (default):

withAuth({
  // ...
  requireAuthForReads: false, // default
})

Using authentication in your frontend

Login form (drop-in UI)

import { LoginForm } from '@airdraft/react-ui'
import '@airdraft/react-ui/styles.css'

export default function LoginPage() {
  return <LoginForm apiUrl={process.env.NEXT_PUBLIC_CMS_API_URL!} />
}

Accept invite form

import { AcceptInviteForm } from '@airdraft/react-ui'

export default function AcceptInvitePage() {
  return <AcceptInviteForm apiUrl={process.env.NEXT_PUBLIC_CMS_API_URL!} />
}

useAuth hook

'use client'
import { useAuth } from '@airdraft/react'

export function UserMenu() {
  const { user, loading, logout } = useAuth()
  if (loading) return null
  if (!user) return <a href="/login">Sign in</a>
  return (
    <div>
      <span>{user.email}</span>
      <button onClick={logout}>Sign out</button>
    </div>
  )
}

The AirdraftProvider automatically refreshes the access token 60 seconds before it expires.

Resources

What's next