TausiDocs

Connectors

Use Tausi users in the database or backend you already have. Two ways, like Clerk and Auth0: signed JWTs that Supabase, Convex, Hasura or Neon trust directly, and webhooks that keep a users table in any database (MySQL, Postgres, MongoDB, ...).

Which one?

You useConnect withWhy
SupabaseJWT (template "supabase")Row Level Security sees the Tausi user in auth.jwt().
ConvexJWT (template "convex")ctx.auth.getUserIdentity() returns the Tausi user.
Hasura, Neon, PostgRESTJWTThey verify tokens against the project's JWKS.
MySQL, MongoDB, Firestore, ...WebhooksKeep your own users table up to date.
Your own APISecret key or JWTverifySession() with a secret key, or verify the JWT yourself.

Both are set up in the dashboard: open the project, then the Connectors tab. Many apps use both: a JWT for reads protected by RLS, and webhooks to copy users into their own tables.

JWTs and templates

Every project is an OpenID Connect issuer. It signs tokens with its own RS256 key and publishes the public keys:

WhatURL
Issuer (iss)https://auth.tausi.app/api/v1/oidc/<projectId>
Discovery…/.well-known/openid-configuration
JWKS…/.well-known/jwks.json

The exact URLs for your project are on its Connectors tab. A token always carries:

ClaimValue
subThe Tausi user id (not a UUID).
sidThe session id; revoking the session stops new tokens.
iss, iat, nbf, exp, jtiSet by Tausi.
audThe template's audience, if it has one.

A JWT template adds an audience, a lifetime (30 s to 24 h) and your own claims. Values can use shortcodes: {{user.email}}, {{user.name}}, {{user.public_metadata.plan}}, {{session.id}} and more. A value that is only a shortcode keeps its type, so {{user.email_verified}} becomes true, not "true". Private metadata is never available: tokens can be read in the browser.

TypeScript
// Cached until 30 s before it expires; null when signed out.
const jwt = await tausi.getJwt({ template: "supabase" });
// Same thing, Clerk-style:
const same = await tausi.getToken({ template: "supabase" });
Rotating the signing key
Rotate on the Connectors tab. New tokens use the new key; the old key stays in the JWKS, so tokens it signed verify until they expire. Delete it afterwards. Verifiers cache the JWKS for a few minutes.

Supabase

  1. Connectors tab: add the Supabase template (audience authenticated, claim role: authenticated, which Supabase requires).
  2. Supabase dashboard → Authentication → Third-party auth → add an OpenID Connect provider with the project's issuer URL.
  3. Give supabase-js the token, and write RLS policies on auth.jwt() ->> 'sub'.
supabase.tsTypeScript
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
  accessToken: () => tausi.getJwt({ template: "supabase" }),
});

const { data } = await supabase.from("todos").select("*"); // only this user's rows
schema.sqlSQL
-- Tausi ids aren't UUIDs: use text columns and auth.jwt() ->> 'sub' (auth.uid() casts to uuid and fails).
create table public.todos (
  id bigint generated always as identity primary key,
  user_id text not null default (auth.jwt() ->> 'sub'),
  title text not null
);
alter table public.todos enable row level security;

create policy "Own todos" on public.todos for all to authenticated
  using (user_id = auth.jwt() ->> 'sub' and auth.jwt() ->> 'iss' = 'https://auth.tausi.app/api/v1/oidc/<projectId>')
  with check (user_id = auth.jwt() ->> 'sub');
Checking iss matters when a Supabase project trusts more than one issuer (several Tausi projects, or Supabase Auth as well): user ids from different issuers could collide.

Convex

Add the Convex template (audience convex), then point Convex at the project's issuer:

convex/auth.config.tsTypeScript
export default {
  providers: [
    {
      type: "customJwt",
      applicationID: "convex",              // = the template's audience
      issuer: "https://auth.tausi.app/api/v1/oidc/<projectId>",
      jwks: "https://auth.tausi.app/api/v1/oidc/<projectId>/.well-known/jwks.json",
      algorithm: "RS256",
    },
  ],
};
convex-client.tsTypeScript
import { ConvexReactClient } from "convex/react";

export const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
convex.setAuth(({ forceRefreshToken }) => tausi.getJwt({ template: "convex", skipCache: forceRefreshToken }));

// convex/todos.ts
export const mine = query(async (ctx) => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not signed in");
  return ctx.db.query("todos").withIndex("by_user", (q) => q.eq("userId", identity.subject)).collect();
});

To also keep a users table in Convex, point a webhook at a Convex HTTP action (see below).

Hasura, Neon and others

Anything that verifies JWTs against a JWKS URL works the same way. Hasura has a preset template (https://hasura.io/jwt/claims); configure HASURA_GRAPHQL_JWT_SECRET with the JWKS URL:

.env.env
HASURA_GRAPHQL_JWT_SECRET={"jwk_url":"https://auth.tausi.app/api/v1/oidc/<projectId>/.well-known/jwks.json","issuer":"https://auth.tausi.app/api/v1/oidc/<projectId>"}

Neon RLS (Neon Authorize), PostgREST, Firebase custom auth bridges and API gateways (Kong, AWS API Gateway, Cloudflare Access) take the same issuer and JWKS URLs.

Webhooks

Add an endpoint on the Connectors tab and choose its events. Tausi POSTs JSON to it, signed with the endpoint's secret (Standard Webhooks format, compatible with Svix libraries).

EventWhendata
user.createdSign-up (email or social)The full user (as the backend API returns it)
user.updatedProfile, metadata, email verified, ban/suspend, two-factorThe full user after the change
user.deletedAccount deleted{ id, deleted: true }
session.createdSign-in{ userId, method, provider? }
session.revokedSign-out or revocation{ userId, sessionId, all }
RequestHTTP
POST https://api.example.com/webhooks/tausi
Content-Type: application/json
webhook-id: msg_2b1f…          # the same on every retry: use it to skip duplicates
webhook-timestamp: 1793980800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{ "type": "user.created", "timestamp": "2026-11-06T12:00:00.000Z", "data": { "id": "…", "email": "asha@example.com", … } }

Answer with any 2xx within 10 seconds. Anything else is retried after 5 s, 5 min, 30 min, 2 h, 5 h and 10 h, then marked failed. You can retry by hand and send a test event from the dashboard, which also shows every delivery for 30 days.

Endpoints must be public https:// URLs. Tausi refuses private, loopback and cloud-metadata addresses and doesn't follow redirects.

Sync to MySQL

server.tsTypeScript
import express from "express";
import mysql from "mysql2/promise";
import { verifyWebhook } from "@tausi-auth/node";

const db = mysql.createPool(process.env.MYSQL_URL!);
const app = express();

// create table users (id varchar(64) primary key, email varchar(255) not null, name varchar(255) not null);
app.post("/webhooks/tausi", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(process.env.TAUSI_WEBHOOK_SECRET!, req.headers, req.body); // raw body!
  } catch {
    return res.status(400).end();
  }
  const u = event.data as { id: string; email: string; name: string };
  if (event.type === "user.created" || event.type === "user.updated") {
    await db.execute(
      "insert into users (id, email, name) values (?, ?, ?) on duplicate key update email = values(email), name = values(name)",
      [u.id, u.email, u.name],
    );
  } else if (event.type === "user.deleted") {
    await db.execute("delete from users where id = ?", [u.id]);
  }
  res.status(204).end();
});

The same handler works for Postgres, MongoDB or Firestore: only the queries change. In a Convex HTTP action, verify with the svix or standardwebhooks package and call a mutation.

Verifying webhooks

The signature is base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw body>" with the secret's bytes (the part after whsec_, base64-decoded). Refuse timestamps more than 5 minutes off. Any Standard Webhooks library does this:

TypeScript
import { verifyWebhook } from "@tausi-auth/node";
const event = verifyWebhook(secret, request.headers, rawBody); // throws if invalid