Skip to content

Users and sign-in

Sign-in settings go in .env next to compose.yaml. Everything else on this page is done in Studio.

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.

Emails are sent through Resend.

  1. In Resend, verify a sending domain and create an API key.
  2. In .env, set RESEND_API_KEY to that key and AUTH_EMAIL_FROM to a sender address on that domain.
  3. 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.

Someone allowed to register applications in your Entra tenant does this:

  1. In the Microsoft Entra admin center, open App registrations and choose New registration.
  2. Pick Accounts in this organizational directory only.
  3. Add a Web redirect URI of <PUBLIC_URL>/api/auth/callback/microsoft-entra-id, such as https://pdf.example.com/api/auth/callback/microsoft-entra-id.
  4. Under Certificates & secrets, create a client secret.
  5. In .env, set ENTRA_TENANT_ID to the directory (tenant) ID, ENTRA_CLIENT_ID to the application (client) ID and ENTRA_CLIENT_SECRET to the secret’s value.
  6. 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.

Only a platform administrator can give someone a password:

  1. In Studio, open Manage → Team and click Set password.
  2. 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.
  3. 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.

Owners and admins invite people in Studio:

  1. Open Manage → Team and click Invite teammate.
  2. Enter the person’s email and a role: member, editor (Content editor), admin or owner. Only owners can invite an owner.
  3. 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.

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 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)

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):

Terminal window
ADMIN_EMAIL=[email protected]
DEPLOY_WORKSPACE=Templates
DEPLOY_API_KEY=pdf_… # generate with: echo "pdf_$(openssl rand -hex 32)"

On a start while the installation has no platform administrator:

  • The ADMIN_EMAIL account 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 from password-account.js.
  • A workspace named DEPLOY_WORKSPACE is 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 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.

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, then docker 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.

On the server, in the installation directory, give the account a new generated password:

Terminal window
docker compose exec api node dist/password-account.js [email protected]

When another platform administrator can sign in, use Set password in Studio instead.

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:

Terminal window
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
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.