Users and sign-in
Sign-in settings go in .env next to compose.yaml. Everything else on this page is done in
Studio.
Choose sign-in methods
Section titled “Choose sign-in methods”Studio offers every method you turn on, and you can combine them. Without email codes or Microsoft, only password accounts can sign in.
| Method | What people get |
|---|---|
| Email codes | A one-time code by email. Invitations are emailed too. |
| Microsoft | Sign in with Microsoft for one Microsoft Entra ID tenant. |
| Passwords | A password a platform administrator sets for them. Always on, no sign-up. |
The settings themselves are listed under configuration.
Email codes
Section titled “Email codes”Emails are sent through Resend.
- In Resend, verify a sending domain and create an API key.
- In
.env, setRESEND_API_KEYto that key andAUTH_EMAIL_FROMto a sender address on that domain. - Run
docker compose up -d.
A code is valid for five minutes and allows three wrong attempts. If Resend cannot send it,
requesting a code answers 503 EMAIL_DELIVERY_UNAVAILABLE.
Microsoft Entra ID
Section titled “Microsoft Entra ID”Someone allowed to register applications in your Entra tenant does this:
- In the Microsoft Entra admin center, open App registrations and choose New registration.
- Pick Accounts in this organizational directory only.
- Add a Web redirect URI of
<PUBLIC_URL>/api/auth/callback/microsoft-entra-id, such ashttps://pdf.example.com/api/auth/callback/microsoft-entra-id. - Under Certificates & secrets, create a client secret.
- In
.env, setENTRA_TENANT_IDto the directory (tenant) ID,ENTRA_CLIENT_IDto the application (client) ID andENTRA_CLIENT_SECRETto the secret’s value. - Run
docker compose up -d.
The default openid, profile and email permissions are enough. The server needs outbound
HTTPS to login.microsoftonline.com and graph.microsoft.com.
Anyone in the tenant can sign in, but sees only the workspaces they are invited to. An existing account with the same email address is reused. Entra’s own MFA and conditional access apply; Studio’s two-factor authentication does not.
Passwords
Section titled “Passwords”Only a platform administrator can give someone a password:
- In Studio, open Manage → Team and click Set password.
- Enter the person’s email address and click Set password. Studio shows a generated password once. Send it to the person over a channel you trust.
- Invite the person to a workspace.
Set password creates the account if needed and signs the person out everywhere. Setting it again is how you reset a password: there is no “forgot password” email. People change their own password under Account → Account security. Passwords are at least 12 characters.
Invite someone
Section titled “Invite someone”Owners and admins invite people in Studio:
- Open Manage → Team and click Invite teammate.
- Enter the person’s email and a role:
member,editor(Content editor),adminorowner. Only owners can invite an owner. - Click Send invitation, or Add invitation when email codes are off.
The person must sign in with exactly the invited address. Without email codes, no email is sent: tell the person to sign in and accept under Account → Your invitations. Someone with no way to sign in yet needs a password first.
On the Team page, select people to Change role, Cancel invitation or Remove from workspace. With email codes on, Resend sends a pending invitation again.
Workspaces and roles
Section titled “Workspaces and roles”Access has two separate layers:
- Workspace roles (
owner,admin,member,editor) decide what someone can do in one workspace. A workspace holds its own templates, files, renders, API keys, webhooks, signing certificates, render limits and audit log. A person can be in several workspaces with a different role in each. What each role can do is in roles. - Platform administrators run the installation: they create workspaces, set render limits, set passwords and grant platform access. Being a workspace owner does not make someone a platform administrator.
Installations are invite-only: signing in gives someone no workspace until they are invited. Whoever creates a workspace becomes its owner, and a workspace always keeps at least one owner. Workspaces cannot be deleted.
Platform administrators
Section titled “Platform administrators”Platform administrators use the same Studio, with more reach:
- The workspace switcher lists every workspace. Choosing one gives full access to it without joining it. Create workspace at the bottom of the switcher adds a new one.
- Manage → Render limits lets them change the workspace’s allowances.
- Manage → Team shows who has platform access. There they set passwords, and select a person to Grant platform admin or Revoke platform admin.
The person must already have an account: they have signed in once, or you set them a password. The last platform administrator cannot be revoked; grant another one first.
The same actions are available over HTTP with a platform administrator’s Studio session. API keys are refused, whatever their scopes. Request and response schemas are in the API reference.
| Method | Endpoint | What it does |
|---|---|---|
GET |
/v1/platform-admins |
List platform administrators |
POST |
/v1/platform-admins |
Grant access: { "email": "..." } or { "userId": "..." } |
DELETE |
/v1/platform-admins/{id} |
Revoke access, by user ID |
POST |
/v1/password-accounts |
Set a generated password: { "email": "..." } |
GET |
/v1/platform/workspaces |
List workspaces with their allowances |
GET |
/v1/platform/workspaces/{id} |
Read one workspace and its allowances |
PUT |
/v1/companies/{id}/allowances |
Replace a workspace’s allowances; {id} is the workspace ID |
GET |
/v1/platform/usage |
Usage of every workspace (see usage) |
Set up from configuration
Section titled “Set up from configuration”A deployment tool can set up a new installation instead of a person, so it can deploy templates
as soon as it starts. Add these to .env before the first start (each is described under
automated setup):
ADMIN_EMAIL=ops@example.comDEPLOY_WORKSPACE=TemplatesDEPLOY_API_KEY=pdf_… # generate with: echo "pdf_$(openssl rand -hex 32)"On a start while the installation has no platform administrator:
- The
ADMIN_EMAILaccount becomes the first platform administrator, and no setup token is printed. The account is created if needed. That person signs in with an email code, Microsoft, or a password frompassword-account.js. - A workspace named
DEPLOY_WORKSPACEis created, owned by that account.
After that, ADMIN_EMAIL is ignored. The API logs the address it made administrator. If it was
mistyped, give that account a password with password-account.js, sign in as it, grant the right
address platform access, then revoke the wrong one.
The deploy key
Section titled “The deploy key”The deploy key is an API key the server creates from DEPLOY_API_KEY, so your deployment
tool can import and publish templates (see deploy across environments).
Studio lists it as Deploy key (DEPLOY_API_KEY). Its scopes and lifetime are in
deploy key. The tool sends the value as its x-api-key header.
An installation has at most one deploy key. On every start the API compares DEPLOY_API_KEY
with it:
DEPLOY_API_KEY is |
What happens |
|---|---|
| Set, no deploy key yet | The key is created in the workspace named by DEPLOY_WORKSPACE. The API refuses to start unless it names exactly one workspace. |
| The same value as before | Nothing changes, even if the key was revoked. |
| A new value | The key is replaced in the same workspace and the old value stops working. |
| Removed | The key keeps working. Revoke it in Studio if it should stop. |
To add a deploy key to an installation that is already set up, set DEPLOY_API_KEY, and set
DEPLOY_WORKSPACE to the name of an existing workspace.
If the key leaks, revoke it under Manage → API keys. It stays revoked until you set a new
value in .env. Give each environment its own value.
If the setup token does not work
Section titled “If the setup token does not work”The first person to enter the setup token from the API’s log becomes the first platform administrator, as in step 6 of the install guide.
- The token scrolled out of the log. It stays the same until setup is done. Run
docker compose restart api, thendocker compose logs api | grep setupToken. - The token stopped working. It is derived from
BETTER_AUTH_SECRET, so changing that setting changes the token. Read it from the log again.
Once any platform administrator exists, setup closes for good and the token is no longer printed. Treat the token as a secret until then.
Recover a locked-out administrator
Section titled “Recover a locked-out administrator”On the server, in the installation directory, give the account a new generated password:
docker compose exec api node dist/password-account.js admin@example.comWhen another platform administrator can sign in, use Set password in Studio instead.
Audit history
Section titled “Audit history”Each workspace has its own append-only audit log of security-relevant changes. Read it in Studio
under Manage → Audit log, where you can filter it and click Export CSV, or with
GET /v1/audit-events and the audit:read scope:
curl "https://pdf.example.com/v1/audit-events?action=template.published&limit=1" \ -H "x-api-key: $API_KEY"{ "items": [ { "id": "0b6a3c1e-5d2f-4c1a-9a3e-2f1d7c8b9e01", "actorType": "user", "actorName": "Ada Lovelace", "action": "template.published", "resourceType": "template", "resourceName": "Invoice", "metadata": { "number": 7 }, "createdAt": "2026-10-07T14:21:08.512Z" } ], "nextCursor": "0b6a3c1e-5d2f-4c1a-9a3e-2f1d7c8b9e01"}Each event also has actorId, actorEmail, resourceId and requestId. metadata differs per
action. GET /v1/audit-events/{id} reads one event. The list is newest first and
paged with limit and cursor. It also takes these filters:
| Query parameter | Description |
|---|---|
action |
One action, such as api_key.revoked |
actorType |
user, api_key or system |
actorId |
The user or API key that acted |
resourceType |
Such as template, api_key or webhook_endpoint |
resourceId |
One resource |
from, to |
ISO 8601 date-times bounding createdAt |
Recorded actions
Section titled “Recorded actions”| Area | Recorded actions |
|---|---|
| Templates | template.created, template.draft_saved, template.content_updated, template.published, template.renamed, template.moved, template.imported, template.deleted |
| Workspace files and folders | workspace_entity.created, workspace_entity.saved, workspace_entity.renamed, workspace_entity.moved, workspace_entity.scoped, workspace_entity.imported, workspace_folder.created, workspace_folder.renamed, workspace_folder.moved, workspace_folder.copied, workspace_folder.deleted, workspace_folder.imported, workspace.imported |
| Assets | asset.created, asset.deleted |
| API keys | api_key.created, api_key.rotated, api_key.revoked, api_key.deleted |
| Webhooks | webhook_endpoint.created, webhook_endpoint.updated, webhook_endpoint.secret_rotated, webhook_endpoint.deleted |
| Signing certificates | signing_certificate.created, signing_certificate.deleted |
| Team | invitation.created, invitation.accepted, invitation.rejected, invitation.canceled, member.role_updated, member.removed, member.left |
| Limits | company.allowances.updated, with the values before and after; company.suspended and company.unsuspended |
Changes the server makes from DEPLOY_API_KEY appear with the actor type system. Renders are
not audit events. Granting and revoking platform administrators and setting passwords belong to no
workspace, so they appear in no workspace’s log.