SemiLayerDocs

Auth & RBAC

SemiLayer uses a layered security model: API keys authenticate the tenant (which environment you're operating against), and optional user JWTs enforce per-row access rules. Together they support everything from a simple public read to complex row-level security.

API Key Types

Each Environment has its own set of API keys. Keys are scoped by environment and by role.

PrefixTypeCan do
sk_dev_Secret, developmentFull read + write access in development
sk_live_Secret, productionFull read + write access in production
pk_dev_Public, developmentRead-only, subject to Lens access rules
pk_live_Public, productionRead-only, subject to Lens access rules
ik_dev_Ingest, developmentIngest webhook trigger only — cannot query
ik_live_Ingest, productionIngest webhook trigger only — cannot query

Secret keys (sk_) bypass all Lens-level access rules. Use them on your backend. Never expose them client-side.

Public keys (pk_) enforce every access rule you declare on the Lens. Safe to include in frontend bundles. If a rule is not met, the request returns 403.

Ingest keys (ik_) can only hit the ingest webhook endpoint (POST /v1/ingest/:lens). They cannot call search, similar, query, or stream.

Manage keys in the Console under Environments → API Keys, or with the CLI:

semilayer key create --type pk --env production
semilayer key list
semilayer key revoke sk_live_abc123

Access Rules

Access rules are declared per-Lens in sl.config.ts under rules. They gate search, similar, query, and subscribe operations.

Rule Types

'public' — No authentication required. Requests with pk_ keys and no user token are allowed.

rules: { search: 'public' }

'authenticated' — A valid user JWT (X-User-Token header or ?userToken=) is required. The JWT is validated against your JWKS endpoint.

rules: { search: 'authenticated' }

ClaimCheck — The user JWT must contain specific claims.

rules: {
  search: {
    authenticated: true,
    claims: { role: ['admin', 'editor'] }  // user's role claim must be 'admin' or 'editor'
  }
}

Function rule — Arbitrary logic. Returns true (allow), false (deny), or { filter: {...} } (allow with a metadata filter applied to results).

rules: {
  search: (claims) => {
    if (!claims.orgId) return false
    return { filter: { orgId: claims.orgId } }  // restrict results to user's org
  }
}
ℹ️

Function rules are serialized and stored in the database. They are evaluated server-side on every request. Only use serializable logic (no closures over external variables).

Per-Operation Rules

You can set different rules for different operations:

lenses: {
  products: {
    // ... fields, facets ...
    rules: {
      search: 'public',          // anyone can search
      similar: 'public',         // anyone can find similar
      query: 'authenticated',    // must be logged in to query
      subscribe: {               // only premium users can live-tail
        authenticated: true,
        claims: { plan: ['pro', 'enterprise'] }
      }
    },
  },
}

query is disabled by default and must be explicitly enabled. All other operations default to 'public' when no rule is set, but require the lens to have the appropriate facet declared.

Dual-Token Pattern

SemiLayer uses a dual-token pattern that separates the tenant identity (API key) from the end-user identity (JWT). This lets you use a single API key per environment while enforcing per-user access rules.

Backend Request:
  Authorization: Bearer pk_live_...     ← identifies the environment
  X-User-Token:  eyJhbGci...           ← identifies the end user (signed by your auth provider)

Your auth provider issues the user JWT (Auth0, Clerk, Supabase Auth, your own system — anything that implements OIDC). Configure the JWKS URL once per Environment, and SemiLayer validates every user token against it.

Configure JWKS

In sl.config.ts:

auth: {
  client: {
    jwt: {
      jwksUrl: 'https://your-issuer/.well-known/jwks.json',
      issuer: 'https://your-issuer/',
      audience: 'https://your-api/',  // optional
    }
  }
}

BeamClient — Attaching a User Token

import { createBeam } from './semilayer'

// One shared BeamClient per environment (reuse across requests)
const beam = createBeam({
  baseUrl: 'https://api.semilayer.com',
  apiKey: 'pk_live_...',
})

// Per-request: bind the user's JWT (cheap — no network, no new socket)
async function handler(req, res) {
  const userBeam = beam.products // or:
  const scoped = await beam.products  // use beam.products directly if no user token needed

  // Use withUser on the underlying client for user-scoped requests:
  const { results } = await beam.products.search({ query: 'shoes' })
  // Without user token: rules evaluated without identity (public rules only)
}
💡

When using the generated Beam client, call withUser on the BeamClient instance inside Beam, then pass it to a fresh Beam construction. Or use BeamClient directly:

import { BeamClient } from '@semilayer/client'

const sharedClient = new BeamClient({ baseUrl, apiKey: 'pk_live_...' })

// Per request:
const userClient = sharedClient.withUser(req.headers['x-user-token'])
const { results } = await userClient.search('products', { query: 'shoes' })

Row-Level Security

Access rule functions can return a filter object that is merged into every query's metadata WHERE clause. This restricts results to rows the user is allowed to see.

rules: {
  search: (claims) => {
    // claims.sub is the user's unique ID from the JWT
    return { filter: { ownerId: claims.sub } }
  }
}

Now any search request returns only records where metadata.ownerId === claims.sub. The filter is applied server-side and cannot be bypassed by the client.

Combined with the query operation:

rules: {
  query: (claims) => {
    if (!claims.sub) return false           // unauthenticated users cannot query
    return { filter: { orgId: claims.orgId } }  // org-scoped rows only
  }
}

RBAC Decision Flow

Request arrives (API key + optional user token)
    │
    ▼
sk_ key?  ──── Yes ────→  Bypass all rules, full access
    │
    No
    ▼
pk_ or ik_ key — resolve Lens + operation
    │
    ▼
No rule set for operation?  → 403 Forbidden
    │
    'public'?  → Allow (no user token needed)
    │
    'authenticated'?  → Validate user JWT → 401 if missing/invalid → Allow
    │
    ClaimCheck?  → Validate JWT → check claims → 403 if no match → Allow
    │
    Function rule?  → Evaluate with JWT claims
                     → return false → 403
                     → return true  → Allow
                     → return { filter } → Allow + apply filter to results

Frontend Safety Checklist

  • Use pk_ keys in browser-side code, never sk_
  • Declare explicit rules for every operation you expose publicly
  • Set minScore on search to prune low-relevance results client-side
  • Use ik_ keys in ingest webhook callers — they cannot query your data
  • Never log or expose sk_ or ik_ keys in client-side bundles