surformSurrealDB schemas, declared

Access (sign-in & tokens)

Accesses are SurrealDB's authentication schemes: they define how tokens are verified and, for your application's end users, how accounts sign up and sign in. Three kinds exist, chosen with type: — record, jwt, and bearer.

Record access: your app's users

The most common kind. A record access turns an email-and-password pair into a record user — SurrealDB runs your SurrealQL to create the account (signup) and to check a sign-in (signin):

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table user {
  schemafull;
  field email: string;
  field password: string;
};

access account {
  on: database;
  type: record;
  signup: {
    create user set
      email = $email,
      password = crypto::argon2::generate($password)
  };
  signin: {
    select * from user
      where email = $email
      and crypto::argon2::compare(password, $password)
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `user` TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE email ON `user` TYPE string;
DEFINE FIELD OVERWRITE `password` ON `user` TYPE string;
DEFINE ACCESS OVERWRITE account ON DATABASE
    TYPE RECORD
    SIGNUP { CREATE user SET email = $email, password = crypto::argon2::generate($password) }
    SIGNIN {
        SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)
    };

No trailing ; inside those captures — the clause's value must be the returned result. $email and $password are what the client sends to the SIGNUP/SIGNIN endpoints.

A post-auth check can inspect the signed-in record and reject with throw:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table user {
  schemafull;
  field email: string;
  field blocked: bool;
};

access gated {
  on: database;
  type: record;
  signin: select * from user where email = $email;
  authenticate: {
    if $auth.blocked { throw 'account suspended'; }
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `user` TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE email ON `user` TYPE string;
DEFINE FIELD OVERWRITE blocked ON `user` TYPE bool;
DEFINE ACCESS OVERWRITE gated ON DATABASE
    TYPE RECORD
    SIGNIN (SELECT * FROM user WHERE email = $email)
    AUTHENTICATE { IF $auth.blocked { THROW 'account suspended' } };

JWT access: verify tokens from somewhere else

The jwt kind does not issue anything (unless given an issuer) — it verifies tokens. The verification source is either a JWKS url: or an algorithm: + key: pair:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

/// Tokens issued by Acme SSO.
access corp_sso {
  on: database;
  type: jwt;
  url: 'https://sso.acme.com/.well-known/jwks.json';
};

access ops {
  on: root;
  type: jwt;
  algorithm: es256;
  key: 'ops_public_key_pem';
  duration { session: 8h; };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE ACCESS OVERWRITE corp_sso ON DATABASE
    TYPE JWT URL 'https://sso.acme.com/.well-known/jwks.json'
    COMMENT 'Tokens issued by Acme SSO.';
DEFINE ACCESS OVERWRITE ops ON ROOT
    TYPE JWT ALGORITHM ES256 KEY 'ops_public_key_pem'
    DURATION FOR SESSION 8h;

One rule governs which keys you may write:

  • Asymmetric verify keys (es*, eddsa, rs*, ps*) are public keys — freely authorable, printed verbatim by the database.
  • Symmetric verify keys (hs*) and every issuer key are secrets. The database redacts them in INFO, and Surform accepts exactly one spelling there: the literal '[REDACTED]'. Writing a real key in those slots is a parse error:
real Surform diagnostics
this schema is wrong on purpose
sdl
namespace acme;
database shop;

access broken {
  on: database;
  type: jwt;
  algorithm: hs256;
  key: 'SUp3r-s3cret-key-material';
};
what Surform reports
text
SDL-E1056 (spec/access.md)

  x access secret key material may not be committed: a symmetric (`HS256`) verify key is secret material — write `key: '[REDACTED]';`
   ,-[8:3]
 7 |   algorithm: hs256;
 8 |   key: 'SUp3r-s3cret-key-material';
   :   ^|^
   :    `-- this key slot takes only `'[REDACTED]'`
 9 | };
   `----
  help: write the literal `key: '[REDACTED]';` in SDL and provision the real secret outside SDL (this access class is run manually, so its secrets are never committed); asymmetric verify keys are
        public and stay authorable

That is deliberate: your schema file lives in git, so secret material must be provisioned outside it — that access class is applied manually, once.

Issuers and audiences

An issuer block turns the access into a token issuer alongside verification. Its key follows the same sentinel rule; its algorithm must equal the verify algorithm when verifying by local key:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

access partner_api {
  on: database;
  type: jwt;
  algorithm: es256;
  key: 'partners_public_key';
  issuer {
    key: '[REDACTED]';
  };
  duration { token: 1h; };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE ACCESS OVERWRITE partner_api ON DATABASE
    TYPE JWT ALGORITHM ES256 KEY 'partners_public_key' WITH ISSUER KEY '[REDACTED]'
    DURATION FOR TOKEN 1h;

audience: (SurrealDB 3.3) — a comma list of strings the token's aud claim must intersect:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

access corp_sso {
  on: database;
  type: jwt;
  url: 'https://sso.acme.com/.well-known/jwks.json';
  audience: 'shop-web', 'shop-mobile';
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE ACCESS OVERWRITE corp_sso ON DATABASE
    TYPE JWT URL 'https://sso.acme.com/.well-known/jwks.json' AUDIENCE 'shop-web', 'shop-mobile';

Bearer access: long-lived grants

A bearer access hands out grant tokens — for user accounts (any level) or record users (database level). Bearer accesses can also issue their own JWTs with a jwt block:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

access ci_tokens {
  on: database;
  type: bearer;
  for: user;
  duration {
    grant: 30d;
    token: 1d;
  };
};

access device_keys {
  on: database;
  type: bearer;
  for: record;
  jwt {
    algorithm: hs256;
    key: '[REDACTED]';
  };
  duration { token: 365d; };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE ACCESS OVERWRITE ci_tokens ON DATABASE
    TYPE BEARER FOR USER
    DURATION FOR GRANT 4w2d, FOR TOKEN 1d;
DEFINE ACCESS OVERWRITE device_keys ON DATABASE
    TYPE BEARER FOR RECORD WITH JWT ALGORITHM HS256 KEY '[REDACTED]'
    DURATION FOR TOKEN 1y;

(365d prints as 1y — SurrealDB's duration canon; the value is unchanged.)

Context: data for the session (SurrealDB 3.3)

context: evaluates at authentication and becomes $session.data — a way to pin per-session facts your queries can read. It requires an access method that authenticates a record (a record access, or a bearer for records), and the expression must be read-only:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table user {
  schemafull;
  field email: string;
  field plan: string;
};

access account_ctx {
  on: database;
  type: record;
  signin: select * from user where email = $email;
  authenticate: {
    if $auth.blocked { throw 'account suspended'; }
  };
  context: { plan: $auth.plan };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `user` TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE email ON `user` TYPE string;
DEFINE FIELD OVERWRITE plan ON `user` TYPE string;
DEFINE ACCESS OVERWRITE account_ctx ON DATABASE
    TYPE RECORD
    SIGNIN (SELECT * FROM user WHERE email = $email)
    AUTHENTICATE { IF $auth.blocked { THROW 'account suspended' } }
    CONTEXT { plan: $auth.plan };

Duration defaults

The three duration sides default to grant 30d, token 1h, session unlimited — and an access without a duration block round-trips to one without. A verify-only access (no issuer) can't issue tokens, so a declared token: side there is dropped by the engine — the linter catches it (SDL-L0011).

Where next