Semaphor
Security

Token Integration

Generating tokens for embedded dashboards with Unified Security

When a connection uses Unified Security, the token request shifts from carrying security policy details to identifying who the user is. Admins define the policies; the token tells Semaphor which actor to resolve them for.

This page covers how to structure token requests for connections in unified mode. For the complete request contract, see the Token API.


What Changes in Unified Mode

In legacy mode, your token request carries security policies (cls, rcls, sls) with concrete parameter values. In unified mode, those policies live on the server as policy definitions and assignments.

Your token request needs to provide:

  1. Actor identity -- who is this user? (tenantId, endUserId, orgUserId)
  2. Security params (optional) -- runtime-owned values for supported {{ placeholder }} parameters, via the securityParams field

That's it. Semaphor resolves the effective security context from persisted assignments plus any securityParams you supply.

Prefer assignment-bound parameters when possible

For the simplest integration, bind parameter values directly in assignments. Your token request stays as simple as passing actor identity, and admins manage all security values in one place. Use securityParams when values genuinely vary per request — such as a user-selected filter or session-specific context.

Row filters accept token values only when the policy says so

A row-level parameter accepts securityParams only when the policy sets its source to Set by the signed token. Values the policy fixes, or that assignments own, cannot be replaced at token time. Connection and schema security keep their existing parameter behavior. See How Row-Level Security Works.

Legacy fields are rejected on unified connections

If a connection is in unified mode, passing cls, rcls, or sls in the token request will fail with: "Unified Security does not support legacy token cls/rcls/sls overlays." Remove these fields from your request body for unified connections.


Actor Identification

The token request identifies the actor using the same fields as before. Semaphor derives the actor type from the fields you provide, checked in this order:

Fields ProvidedActor TypeDescription
orgUserIdORG_USEROrganization-level user
tenantId + endUserIdTENANT_USEREnd user within a specific tenant
tenantId aloneTENANTTenant-level actor (no specific user)

If none of these fields are present on a unified connection, the request fails with: "Unified Security requires an organization, tenant, or tenant user actor context."

These fields work on both dashboard tokens and project tokens. See Token API for the complete field reference.


Integration Scenarios

The admin creates policy definitions and assignments with all parameter values bound. Your token request only needs actor identity.

const TOKEN_URL = 'https://semaphor.cloud/api/v1/token';

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',       
  endUserId: 'user_jane',        
  // No cls, rcls, sls, or security params needed.
  // Admin configured everything via policy definitions and assignments.
};

async function fetchToken() {
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestBody),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const token = await response.json();
  return token;
}

This is the simplest integration — and the recommended approach for most use cases. The admin manages all security values centrally, and your code has zero security logic.

Scenario 2: Admin Policy Definitions + Runtime Security Params

When some values genuinely vary per request and can't be pre-configured in assignments, you can supply them at token time via the securityParams field. The admin creates the policy structure and binds stable values in assignments; your code fills in the dynamic ones.

For example, the admin declares tenant_id as assignment-owned and declares region and department as token-owned because they change per session.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  securityParams: {                   
    region: 'west',                   
    department: 'engineering',        
  },                                  
};

async function fetchToken() {
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestBody),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

For persisted RLS, these keys fill only parameters declared with source: TOKEN. Parameter ownership is deterministic; Semaphor does not inspect SQL to decide whether a token value narrows or widens access.

Scenario 3: No Applicable Assignment

If an actor has no applicable assignment, that actor does not receive the corresponding persisted policy definition.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_new_customer',
  endUserId: 'user_bob',
  // This actor has no assignments yet.
  // No persisted Unified Security definition is applied for this actor.
};

This does not mean the actor has unrestricted product access. Connection permissions, database grants, legacy or token-only security where applicable, and other authorization controls remain independent.

Audit before switching modes

Before switching a connection to unified, verify that every intended actor has the correct assignments. Use Security preview in the console to inspect representative actors.


The securityParams Field

In unified mode, the securityParams field supplies supported runtime parameter values. For persisted RLS, the definition must explicitly declare each supplied key with source: TOKEN.

securityParams vs params

The securityParams field is for Unified Security placeholder values (CLS/SLS/RLS templates). The separate params field is for initial report-session inputs such as calendarContext and valueFormatPreferences. See Report Preferences and the Token API for the full behavior and field reference.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  securityParams: {                   
    department: 'sales',              // fills {{ department }} placeholder
    access_tier: 'standard',          // fills {{ access_tier }} placeholder
  },                                  
};

Each key in securityParams matches a {{ placeholder }} name in the policy definition. If the placeholder uses @secret, see the next section for how secrets are handled.


Secret Parameters (@secret)

Policy definitions can mark sensitive placeholders with an @secret suffix -- for example, {{ password@secret }} in a CLS connection template. Secret values are passed in a separate secretSecurityParams field on the token request, not in securityParams. Semaphor encrypts them server-side so the plaintext never appears in the signed JWT.

How it works

  1. You send the plaintext secret in the secretSecurityParams field, using the plain key name (without the @secret suffix).
  2. Non-secret values go in securityParams as usual.
  3. Semaphor validates the secret against applicable CLS templates, stores it server-side, and replaces it with an opaque lookup reference.
  4. The signed JWT carries the reference, not the plaintext secret.
  5. At query time, Semaphor resolves the real secret server-side when rendering the CLS template.

The plaintext secret is never embedded in the token payload.

Example

Given this CLS policy definition:

CLS policy definition
{
  "connectionTemplate": "postgresql://{{ username }}:{{ password@secret }}@db.example.com/app"
}

Your token request separates plain values from secrets:

Token request
const response = await fetch('https://semaphor.cloud/api/v1/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'project',
    projectId: PROJECT_ID,
    projectSecret: PROJECT_SECRET,
    tenantId: 'tenant_acme',
    endUserId: 'user_123',
    securityParams: {                                  
      username: 'analytics_user',                      
    },                                                 
    secretSecurityParams: {                            
      password: 'super-secret-db-password',            
    },                                                 
  }),
});

The resulting JWT payload contains a reference for the secret, not the plaintext:

JWT payload (conceptual)
{
  "type": "project",
  "project_id": "p_123",
  "tenantId": "tenant_acme",
  "endUserId": "user_123",
  "securityParams": {
    "username": "analytics_user",
    "password": "9c4d2c91-5f2a-4d8d-b6d0-3d6d1d5d4e11"
  }
}

Notice that both values end up under securityParams in the JWT -- but the password is an opaque reference, not the plaintext. At query time, Semaphor resolves 9c4d2c91-... back to the real password server-side and renders the connection string.

Key naming

Always use the plain key name -- write password, not password@secret. The @secret suffix only appears in the policy definition template to tell Semaphor how to handle the value. Plain values go in securityParams; secret values go in secretSecurityParams.

Do not mix secret and plain keys

A key cannot appear in both securityParams and secretSecurityParams. If a key matches an @secret placeholder in a CLS template, it must be in secretSecurityParams -- passing it in securityParams will return an error.


Dashboard Tokens vs Project Tokens

Both token types work with Unified Security. The difference is scope, not security behavior.

Dashboard TokenProject Token
CredentialsdashboardId + dashboardSecretprojectId + projectSecret
Actor identitytenantId, endUserIdorgUserId, tenantId, endUserId
Security paramssecurityParams fieldsecurityParams field
Secret security paramssecretSecurityParams fieldsecretSecurityParams field
Security modeRespects per-connection modeRespects per-connection mode
Mixed modesWorks (per-connection)Works (per-connection)

When a project has some connections in legacy mode and others in unified mode, both are handled correctly within the same token. Each connection resolves security independently based on its own mode.


What NOT to Do

Do not mix legacy security fields with unified connections

The following token request will be rejected if the connection is in unified mode:

// This will fail on unified connections
const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  cls: {                              
    name: 'store_sales_primary',      
    params: { tenant: 'acme' },       
  },                                  
  rcls: {                             
    name: 'region_filter',            
    params: { state: ['California'] },
  },                                  
};

Remove cls, rcls, and sls from your request body. Use the securityParams field for runtime security values instead.

If you need legacy security fields for some connections, keep those connections in legacy mode. Per-connection mode switching lets you migrate incrementally.


Best Practices

  • Bind stable values in assignments. If a value is fixed per actor — database name, tenant ID, schema — put it in the assignment. This keeps your token code simple and gives admins centralized control over security configuration.
  • Use securityParams for genuinely dynamic values. Values that change per request or per session (e.g., a user-selected region filter, a time-scoped access window) are a good fit for securityParams. There is no performance penalty for plain security params — they pass through to the token without additional processing.
  • Prefer assignment-bound secrets over token-time secrets. Secret parameters (@secret) passed via secretSecurityParams require server-side encryption and storage on every token request. Binding secrets in assignments avoids this per-request overhead and keeps sensitive values out of your application code entirely.

Error Reference

Common errors when using Unified Security with tokens:

Error MessageCauseSolution
"Unified Security requires an organization, tenant, or tenant user actor context"No actor identity fields in the token requestPass orgUserId, tenantId, or tenantId + endUserId
"Unified Security does not support legacy token cls/rcls/sls overlays"Legacy cls, rcls, or sls fields on a unified connectionRemove these fields; use securityParams for runtime security values
"placeholder 'X' is required but no value was provided"A policy definition placeholder has no value from assignments or securityParamsSupply the missing value in the securityParams field
"secret placeholder 'X' could not be resolved"A @secret parameter references an invalid or missing secretCheck the secret configuration in the admin UI
"Unified Security actor validation failed"The actor (org user, tenant, or tenant user) does not exist or does not belong to the projectVerify the actor exists and is associated with the project

Migration Checklist

When moving a connection from legacy to unified:

  1. Create policy definitions with the same policy logic you had in token-based CLS/RCLS/SLS.
  2. Create assignments for each actor, binding the parameter values that were previously in your token requests.
  3. Use Security preview to verify each actor's effective security matches your expectations.
  4. Remove cls, rcls, and sls fields from your token generation code. Move any runtime security values to securityParams.
  5. Switch the connection to unified mode.
  6. Test with each actor type to confirm correct behavior.

For a detailed migration walkthrough, see the Migration Guide.


Next Steps

On this page