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 use | Connect with | Why |
|---|---|---|
| Supabase | JWT (template "supabase") | Row Level Security sees the Tausi user in auth.jwt(). |
| Convex | JWT (template "convex") | ctx.auth.getUserIdentity() returns the Tausi user. |
| Hasura, Neon, PostgREST | JWT | They verify tokens against the project's JWKS. |
| MySQL, MongoDB, Firestore, ... | Webhooks | Keep your own users table up to date. |
| Your own API | Secret key or JWT | verifySession() 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:
| What | URL |
|---|---|
| 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:
| Claim | Value |
|---|---|
| sub | The Tausi user id (not a UUID). |
| sid | The session id; revoking the session stops new tokens. |
| iss, iat, nbf, exp, jti | Set by Tausi. |
| aud | The 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.
// 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" });Supabase
- Connectors tab: add the Supabase template (audience authenticated, claim role: authenticated, which Supabase requires).
- Supabase dashboard → Authentication → Third-party auth → add an OpenID Connect provider with the project's issuer URL.
- Give supabase-js the token, and write RLS policies on auth.jwt() ->> 'sub'.
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-- 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');Convex
Add the Convex template (audience convex), then point Convex at the project's issuer:
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",
},
],
};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:
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).
| Event | When | data |
|---|---|---|
| user.created | Sign-up (email or social) | The full user (as the backend API returns it) |
| user.updated | Profile, metadata, email verified, ban/suspend, two-factor | The full user after the change |
| user.deleted | Account deleted | { id, deleted: true } |
| session.created | Sign-in | { userId, method, provider? } |
| session.revoked | Sign-out or revocation | { userId, sessionId, all } |
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.
Sync to MySQL
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:
import { verifyWebhook } from "@tausi-auth/node";
const event = verifyWebhook(secret, request.headers, rawBody); // throws if invalid