Give CI workspace access without sharing your cookie.
A Personal Access Token (PAT, prefix `pcft_live_*`) is the right shape for any non-human caller: deploy pipelines, infra-as-code, scheduled exporters, admin scripts.
1
When to mint a PAT
- Deploy pipeline needs to provision an Auth app for the new environment + push templates.
- Infra-as-code (Terraform, Pulumi) manages workspaces + roles declaratively.
- Scheduled job exports the message log nightly for compliance / analytics.
- Admin script a human runs from their laptop but doesn't want their cookie embedded in the script.
Don't mint a PAT for:
- End-user identity. Customers' end- users go through Auth, not the Platform API.
- Day-to-day operator workflows. Sign in to the console; that's what it's for.
2
Mint a PAT
PATs bind to one or more managed workspace policies(rows in workspace_policy). Author the policy first, then reference its id when minting. The two-step shape — instead of an inline policy on mint — means a teammate can rotate the PAT without re-asserting the permission set, and a single policy can be reused across many PATs and roles.
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/policies \
-H 'authorization: Bearer <cookie-or-existing-PAT>' \
-H 'content-type: application/json' \
-d '{
"name": "ci-staging-deploy",
"description": "Auth app provisioning + Mail template ops for CI",
"policy": [
{
"effect": "allow",
"actions": ["auth.create", "auth.read", "auth.update"],
"resources": ["pcft:heimdall:app/*"]
},
{
"effect": "allow",
"actions": ["mail.read", "mail.send"],
"resources": ["*"]
}
]
}'
# → { "id": "<policy-uuid>", "workspace_id": "...", ... }
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/api-keys \
-H 'authorization: Bearer <cookie-or-existing-PAT>' \
-H 'content-type: application/json' \
-d '{
"name": "github-actions-staging-deploy",
"description": "Deploys acme-staging app from CI",
"policy_ids": ["<policy-uuid-from-step-1>"]
}'Response — the only time the plaintext token is returned:
{
"token": "pcft_live_thatappearsonce...",
"record": {
"id": "<pak-uuid>",
"workspace_id": "<workspace-uuid>",
"created_by": "<account-uuid>",
"name": "github-actions-staging-deploy",
"description": "Deploys acme-staging app from CI",
"token_prefix": "pcft_live_thata",
"policies": [
{ "id": "<policy-uuid>", "name": "ci-staging-deploy", "description": "..." }
],
"last_used_at": null,
"revoked_at": null,
"created_at": "2026-05-25T...",
"updated_at": "2026-05-25T..."
}
}Platform-Auth stores only the hash. No recovery — if you lose the secret, rotate (Section 5) and update your secret store. Treat the token like a password.
Mint requires a human session. PAT minting is cookie/JWT-only on api.platform-auth.productcraft.co — a PAT cannot mint another PAT. Same applies to workspace create/delete and role/policy/invite mutations. Routine reads and PAT-introspect (the operations downstream services need) do work with a PAT bearer.
3
Caller-narrowing on mint
You cannot grant a PAT permissions you don't yourself hold.
Same shape as role authoring (Chapter 2 — Section 5). An admin (excluded from workspace.delete) can't mint a PAT with workspace.delete in its policy. Trying returns 403 Cannot grant actions you don't have: workspace.delete.
Implication for CI: if your pipeline needs broad permissions, mint the PAT from an owner account, not from a least-privilege admin. The PAT inherits the minter's authority cap.
4
Scope tightly
Patterns for common pipelines:
{
"effect": "allow",
"actions": ["workspace.audit.read", "mail.message.read"],
"resources": ["*"]
}{
"effect": "allow",
"actions": ["auth.create", "auth.update", "auth.read"],
"resources": ["pcft:heimdall:app/staging-*"]
}[
{
"effect": "allow",
"actions": ["mail.*"],
"resources": ["*"]
},
{
"effect": "deny",
"actions": ["mail.delete"],
"resources": ["*"]
}
]Explicit deny wins. The template-deploy PAT above can create, update, send, but not delete — even if a teammate later widens mail.* to include something destructive.
5
Rotate
Periodic rotation (90 days is a reasonable default) + on-suspicion rotation when a former teammate leaves or a secret might have leaked.
# Mint replacement first — re-uses the same managed policy from Section 2.
curl -X POST .../v1/workspaces/acme/api-keys \
-H 'authorization: Bearer <admin-or-owner-cookie-or-jwt>' \
-d '{ "name": "github-actions-staging-deploy-v2", "policy_ids": ["<policy-uuid>"] }' > new.json
# Update CI secret store with the new value
# After CI is using the new PAT, revoke the old
curl -X DELETE .../v1/workspaces/acme/api-keys/<old-id> \
-H 'authorization: Bearer <admin-or-owner-cookie-or-jwt>'Revocation is immediate at the database, but every service that accepts PAKs caches introspection results for 60 seconds. A revoked PAK can still authenticate against an in-flight cached pod for up to 60s. Acceptable for most cases; if you need instant fanout, see task tasks/platform-auth-api/004-pak-revocation-fanout.md.
6
Audit usage
Every successful PAK call writes an audit row tagged with the PAK id and the caller IP. Useful for spotting unused PAKs (candidate for revocation), or for incident response after a suspected leak.
curl 'https://api.platform-auth.productcraft.co/v1/workspaces/acme/audit-logs?actor_id=<pak-id>&limit=50' \
-H 'authorization: Bearer <cookie-or-pak>'Plus the last_used_at field on the PAK row — displays in the console. PATs with no usage in > 30 days are candidates for cleanup.
7
Where to store the secret
- GitHub Actions: repo or org-level secret, accessed via
${{ secrets.PRODUCTCRAFT_PAT }}. - GitLab CI: CI/CD variables, masked.
- Vault / Doppler / AWS Secrets Manager: the right choice if you have multiple environments + CI + production services all needing tokens.
- .env files committed to a repo: never. Even private repos. The audit trail of who-saw-what is hopeless once it lands in git.
8
Provisioning script — a complete example
name: Provision staging
on:
workflow_dispatch:
jobs:
provision:
runs-on: ubuntu-latest
steps:
- name: Create Auth app for staging
env:
PAT: ${{ secrets.PRODUCTCRAFT_PAT }}
run: |
curl -fsS https://api.platform-auth.productcraft.co/v1/workspaces/acme/apps \
-H "authorization: Bearer $PAT" \
-H 'content-type: application/json' \
-d '{ "slug": "acme-staging", "display_name": "Acme (staging)" }'
- name: Sync role catalogue
env:
PAT: ${{ secrets.PRODUCTCRAFT_PAT }}
run: |
curl -fsS https://api.auth.productcraft.co/v1/apps/<id>/roles \
-H "authorization: Bearer $PAT" \
-d '{ "name": "viewer", "description": "Read-only" }' || true
- name: Push Mail templates from repo
env:
PAT: ${{ secrets.PRODUCTCRAFT_PAT }}
run: |
for t in templates/*.yaml; do
mail-cli templates upsert -f "$t" --workspace acme --pak "$PAT"
doneOne PAT, narrowly scoped (auth.create + mail.* on the workspace), workflow-dispatch only. Audit trail records every successful action with the PAT id + the GitHub Actions runner IP.