Authentication

Available on request

This unified authentication model is currently rolling out and only enabled for selected environments. If you'd like early access, email humans@tiptap.dev. Until your environment is migrated, continue using the existing per-service authentication for Collaboration, Conversion, and Content AI.

Tiptap services use JWTs (JSON Web Tokens) for authentication and authorization. You generate and sign tokens on your server, then pass them to Tiptap services. Each token identifies your environment, names the services it can access, and the actions it's authorized to perform on your resources.

How it works

  1. You create an asymmetric secret key pair in your Tiptap dashboard.
  2. Your server signs a JWT using the private key.
  3. Your client passes the JWT when connecting to a Tiptap service (AI, Convert, Documents).
  4. Tiptap verifies the token's signature with the public key and checks your subscription.

Why a new auth model?

The new model replaces per-service tokens with a single, unified JWT that carries everything a request needs to know. Which services it can reach, what it's allowed to do, and on which resources. The result is a smaller integration surface, stronger defaults, and finer-grained control.

  • One token, many services. A single JWT authorizes any combination of AI, Convert, and Documents through the aud claim.
  • Cross-service workflows. The AI service can act on your documents under the same token. No service-to-service plumbing.
  • Permissions inside the token. Declare exactly what's allowed via action, resource, and optional constraints, instead of relying on coarse service-level scopes.
  • Zero-downtime key rotation. Run multiple active key pairs per environment and retire old keys without a switchover window.

Claims reference

Every JWT must include these registered claims:

ClaimTypeRequiredDescription
issstringYesYour environment hash ID (from the Tiptap dashboard)
audstring | string[]YesWhich services this token can access: "AI", "Convert", "Documents"
iatnumberNoIssued-at timestamp (epoch seconds)
expnumberYesExpiration timestamp (epoch seconds)
substringNoUser identifier (for audit trails and tracing)
permissionsPermission[]NoWhat this token is allowed to do. Omitting grants no specific actions.

Signing the token

Asymmetric key pair (ES256) — default

When you create a secret in the dashboard, you receive an ECDSA private key (P-256). You sign tokens with your private key. Tiptap only stores the public key and uses it to verify tokens. Your private key never leaves your server.

import { SignJWT, importPKCS8 } from 'jose'

const privateKey = await importPKCS8(process.env.TIPTAP_PRIVATE_KEY, 'ES256')

const jwt = await new SignJWT({
  permissions: [
    { action: 'Documents:Read', resource: '*' },
  ],
})
  .setProtectedHeader({ alg: 'ES256' })
  .setIssuer('your-environment-hash-id')
  .setAudience(['Documents'])
  .setIssuedAt()
  .setExpirationTime('30m')
  .sign(privateKey)

Other languages

The JWT is a standard RFC 7519 token. Any JWT library that supports ES256 can sign tokens. The payload structure is the same regardless of language.

Python (PyJWT, requires pyjwt[crypto] for ES256):

import jwt, time

with open("private_key.pem", "rb") as f:
    private_key = f.read()

payload = {
    "iss": "your-environment-hash-id",
    "aud": ["AI", "Documents"],
    "iat": int(time.time()),
    "exp": int(time.time()) + 1800,
    "permissions": [
        {"action": "Documents:Read", "resource": "*"}
    ]
}

token = jwt.encode(payload, private_key, algorithm="ES256")

PHP (firebase/php-jwt):

use Firebase\JWT\JWT;

$privateKey = file_get_contents('private_key.pem');

$payload = [
    'iss' => 'your-environment-hash-id',
    'aud' => ['AI', 'Documents'],
    'iat' => time(),
    'exp' => time() + 1800,
    'permissions' => [
        ['action' => 'Documents:Read', 'resource' => '*'],
    ],
];

$token = JWT::encode($payload, $privateKey, 'ES256');

Permissions

Permissions define what a token is allowed to do. While they are optional, a token without permissions authenticates the request but grants no specific actions.

Each permission has an action and a resource (both required), plus optional constraints:

{
  "permissions": [
    {
      "action": "Documents:Read",
      "resource": "*",
      "constraints": { "prefix": "team1_" }
    }
  ]
}

Actions

Actions follow the format Service:Operation. Each Tiptap service defines its own actions:

ServiceActions
DocumentsDocuments:Read, Documents:Write, Documents:Comment
Documents REST APIDocuments:Api:All grants full access to the Document Server's REST API.
ConvertConvert:Import:Docx, Convert:Export:Docx, Convert:Import:Markdown, Convert:Export:Markdown, Convert:Export:Doc, Convert:Export:Odt, Convert:Export:Epub, Convert:Export:Pdf, Convert:Fonts
AIAI:Generation, AI:Toolkit

Documents:Write implies Documents:Read and Documents:Comment — granting write access automatically grants the other two, so you don't need to list them separately.

Documents:Api:All grants full access to the Document Server's REST API. It's separate from the other Documents:* actions, which authorize document operations over the collaboration connection.

Convert:* actions are scoped by file format and operation (Import vs. Export). A token only needs the actions for what it actually does — e.g. a token that only exports PDF needs Convert:Export:Pdf. Today only DOCX and Markdown support both directions; Doc, ODT, EPUB, and PDF are export-only. Convert:Fonts is unscoped and applies to the /fonts/* routes.

AI:Generation covers the text and image generation, suggestions, agent, and audio/speech endpoints. AI:Toolkit covers the toolkit endpoints. Neither action implies the other. AI actions are not scoped by resource today, so use "resource": "*" on AI permissions.

Actions are case-insensitive — "Documents:Read" and "documents:read" are equivalent. Resources and constraint values (prefix, suffix, in) remain case-sensitive because they can reference externally meaningful identifiers. Pick a consistent casing for your own code.

Resources

The resource field is required on every permission. It controls which resources the permission applies to:

ValueMeaningExample
"*"Wildcard — matches everything{ "action": "Documents:Read", "resource": "*" }
"*" + constraintsFiltered wildcard — matches resources that pass the constraints{ "action": "Documents:Read", "resource": "*", "constraints": { "prefix": "team_" } }
"doc_42"Specific — matches only this exact resource{ "action": "Documents:Write", "resource": "doc_42" }

Key rules:

  • Use "*" for wildcard access. A wildcard without constraints matches everything.
  • A specific resource matches only that exact resource.

Constraints

Constraints add fine-grained filtering on top of resource matching. They're useful when you want to allow access to a category of resources rather than listing each one.

{
  "action": "Documents:Read",
  "resource": "*",
  "constraints": { "prefix": "team1_" }
}

This grants read access to any document whose name starts with "team1_".

Available constraint fields:

FieldTypeDescription
prefixstringResource name must start with this value
suffixstringResource name must end with this value
instring[]Resource name must be one of these exact values

Combining fields within a single constraint uses AND logic (all must pass):

{ "prefix": "team1_", "suffix": "_published" }

Matches "team1_report_published" but not "team1_report_draft".

An array of constraints uses OR logic (any one passing is enough):

[
  { "prefix": "team1_" },
  { "prefix": "team2_" }
]

Matches "team1_doc" or "team2_doc".

Rules:

  • The constraints field can't be empty. Each constraint must declare at least one of prefix, suffix, or in, and an array of constraints must contain at least one entry. Omit the field entirely for "no filtering".
  • in cannot be combined with prefix or suffix in the same constraint object. Use separate constraints in an array instead.
  • prefix and suffix must be non-empty strings.
  • in must be a non-empty array of strings.

Examples

Full access to all services

{
  "iss": "env_abc123",
  "aud": ["AI", "Convert", "Documents"],
  "iat": 1722344565,
  "exp": 1722344865,
  "permissions": [
    { "action": "AI:Generation", "resource": "*" },
    { "action": "AI:Toolkit", "resource": "*" },
    { "action": "Documents:Write", "resource": "*" },
    { "action": "Convert:Import:Docx", "resource": "*" },
    { "action": "Convert:Export:Docx", "resource": "*" },
    { "action": "Convert:Import:Markdown", "resource": "*" },
    { "action": "Convert:Export:Markdown", "resource": "*" },
    { "action": "Convert:Export:Doc", "resource": "*" },
    { "action": "Convert:Export:Odt", "resource": "*" },
    { "action": "Convert:Export:Epub", "resource": "*" },
    { "action": "Convert:Export:Pdf", "resource": "*" },
    { "action": "Convert:Fonts", "resource": "*" }
  ]
}

Documents:Write covers read and comment by implication, so only one Documents entry is needed.

Read-only access to specific documents

{
  "permissions": [
    {
      "action": "Documents:Read",
      "resource": "*",
      "constraints": {
        "in": ["document_a", "document_b"]
      }
    }
  ]
}

AI access only

Grant the AI features the token needs. The example below allows both generation and toolkit endpoints:

{
  "aud": ["AI"],
  "permissions": [
    { "action": "AI:Generation", "resource": "*" },
    { "action": "AI:Toolkit", "resource": "*" }
  ]
}

Document conversion

Grant only the format/direction pairs the token actually uses. The example below imports DOCX and exports PDF:

{
  "aud": ["Convert"],
  "permissions": [
    { "action": "Convert:Import:Docx", "resource": "*" },
    { "action": "Convert:Export:Pdf", "resource": "*" }
  ]
}

Scoped by document prefix

Allow reading and commenting on documents that belong to a specific team:

{
  "permissions": [
    {
      "action": "Documents:Read",
      "resource": "*",
      "constraints": { "prefix": "team-sales_" }
    },
    {
      "action": "Documents:Comment",
      "resource": "*",
      "constraints": { "prefix": "team-sales_" }
    }
  ]
}

Write access to a single document

{
  "permissions": [
    { "action": "Documents:Write", "resource": "meeting-notes-2024" }
  ]
}

Security best practices

  • Keep tokens short-lived. Set exp to 30 minutes or less. Generate a fresh token for each session or connection.
  • Use the least privilege. Only include the permissions and audiences the token actually needs. A token used only for Convert exports shouldn't include "Documents" or "AI" in its audience.
  • Never expose your private key client-side. Token generation must happen on your server. Send the signed JWT to your client, never the signing key.
  • Rotate keys periodically. Create a new key pair, update your server to sign with the new key, then retire the old one. Tiptap supports multiple active key pairs per environment to allow zero-downtime rotation.
  • Set sub for auditability. Including a user identifier in sub helps trace actions back to specific users in logs and audit trails.