Authentication and Roles
Set up login for CMS editors, manage team members, assign roles, and invite collaborators.
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
- Click Invite member.
- Enter the person's email address.
- Select a role (admin, publisher, or editor).
- 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
- Find the member in the Members list.
- Click the role badge next to their name.
- Select the new role.
Changes take effect immediately.
Removing a member
- Find the member in the Members list.
- Click ⋯ → Remove from team.
- 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
@airdraft/plugin-authon npm —withAuth,withAutoAuth,CredentialsProvider,GitHubOAuthProvider,GoogleOAuthProvider,UserStore@airdraft/authon npm — low-level JWT utilities,ROLE_PERMISSIONS,can(user)@airdraft/react-uion npm —LoginForm,AcceptInviteForm@airdraft/reacton npm —useAuthhook- GitHub — aevrHQ/airdraft