@kuratchi/auth
v0.0.4
Published
Config-driven auth for KuratchiJS — credentials, OAuth, RBAC, rate limiting, Turnstile
Readme
@kuratchi/auth
The Kuratchi quickstart auth library. Pick this when you want a working credentials-based auth in five minutes — signup, signin, sessions, role-based permissions, rate limiting, OAuth, Turnstile bot protection — all driven by a single config object.
What this is
A starter, not a standard. @kuratchi/auth is opinionated, batteries-included, and tightly scoped to D1-backed apps. It gets you to "users can log in" fast. If you outgrow it (federated identity, complex authorization graphs, custom session backends), swap it out — the framework doesn't depend on it.
What this is not
- The auth library you should use for everything. For production apps with serious requirements, evaluate Auth.js with a Workers adapter, Lucia, WorkOS, Clerk, or your own implementation.
- A framework feature.
@kuratchi/jsdoesn't know@kuratchi/authexists. The integration point is a regular middleware step, just like any third-party auth library would have. - Cloudflare Access. Access is the platform's edge identity layer and lives in
@kuratchi/js/accessbecause it's a framework primitive (no DB, no sessions, no schemas — just JWT verification). The two compose orthogonally — see Composing with Cloudflare Access below.
If "starter, not standard" feels too small, that's the signal to use something else. The framing matters because:
- It keeps
@kuratchi/authsimple. We don't have to support every SSO + MFA + WebAuthn flow under the sun. - It keeps the framework decoupled. Swapping auth libraries shouldn't require a rewrite.
- It sets honest expectations. You're using a quickstart, not a battle-hardened identity platform.
Install
npm install @kuratchi/auth @kuratchi/ormWire it up
For KuratchiJS, the primary adapter is @kuratchi/auth/kuratchi-js. Auth is still composed at middleware time, not declared in kuratchi.config.ts.
// src/server/auth-config.ts
import { kuratchiAuthConfig } from '@kuratchi/auth/adapter';
export const authConfig = kuratchiAuthConfig({
cookieName: 'kuratchi_session',
sessionEnabled: true,
csrf: {
paths: ['/auth/signin', '/auth/signup'],
trustedOrigins: ['https://app.example.com'],
},
guards: {
paths: ['/*'],
exclude: ['/auth/signin', '/auth/signup', '/api/public/*'],
redirectTo: '/auth/signin',
},
});// src/middleware.ts
import { defineMiddleware } from '@kuratchi/js';
import { initKuratchiAuth } from '@kuratchi/auth/kuratchi-js';
import { authConfig } from '$server/auth-config';
const auth = initKuratchiAuth(authConfig);
export default defineMiddleware({
auth: auth.middleware(),
});That is the primary KuratchiJS integration point. @kuratchi/auth/middleware remains available as the lower-level compatibility path.
Common APIs
import {
// sessions + sign-up/sign-in
signUp, signIn, signOut, getCurrentUser, getAuth,
// password reset
requestPasswordReset, resetPassword,
// email verification
verifyEmail, resendEmailVerification,
// organization invitations (org-mode)
inviteMember, acceptInvite,
// activity, RBAC, OAuth, guards, turnstile
logActivity, getActivity,
hasRole, hasPermission, assignRole,
startOAuth, handleOAuthCallback,
requireAuthGuard, verifyTurnstile,
// org-DO helpers (used by your custom resolvers)
getOrgClient, getOrgStubByName, resolveOrgDatabaseName, isOrgAvailable,
// schema table maps to spread into your ORM schemas
authAdminTables, authOrgTables,
} from '@kuratchi/auth';All of these read from request-scoped locals populated by kuratchiAuthMiddleware. Call them from route handlers, actions, RPC functions — the auth context is available wherever the framework runs request code.
Feature areas
- Credentials: signup, signin, signout, current user, password reset.
- Email verification: optional
sendVerificationEmailcallback;requireEmailVerifiedblocks signin until the user clicks the link;verifyEmail()/resendEmailVerification()form-actions. - Invitations (org-mode):
inviteMember()creates a placeholder user inside the org's DO + anorganizationUsersmapping + an invite token.acceptInvite()finishes the flow. - Organizations (multi-tenant): one Durable Object per org. The package routes credential/session ops to the right DO via
getOrgStubByName(doName). Admin D1 keeps only the email→org index. See Organizations and Schema and the reference implementation inapps/web/src/server/auth.do.ts. - Activity logging: structured audit trail with optional severity / category metadata.
- Roles and permissions: define role → permission maps; check via
hasRole/hasPermission. - OAuth providers: opinionated integrations for Google, GitHub.
- Route guards: declarative path-based authentication redirects.
- CSRF and origin checks: opt-in Fetch Metadata and
Originvalidation for credential POST routes. - Rate limiting: per-route throttling backed by a KV namespace.
- Turnstile: Cloudflare bot-check verification on protected routes.
Schema
Spread the package's table maps into your ORM schemas:
// src/server/schemas/admin.ts (single-tenant: keep all auth tables here)
import { authAdminTables } from '@kuratchi/auth';
export const adminSchema = {
name: 'admin',
tables: {
...authAdminTables,
// your app tables
},
};For org-mode you split into two schemas — admin keeps organizations + organizationUsers + emailVerifications, the per-org DO holds users + sessions + passwordResets. See the Organizations and Schema docs for the canonical split.
Org-mode at a glance
// src/server/auth-config.ts
export const authConfig = kuratchiAuthConfig({
credentials: { binding: 'DB', requireEmailVerified: true,
sendVerificationEmail: async ({ email, token }) => { /* ... */ },
sendInviteEmail: async ({ email, token, invitedBy }) => { /* ... */ },
},
organizations: { binding: 'ORG_DB' }, // ← turns org-mode on
});// src/server/auth.do.ts — the per-org DO. You write this; the package calls into it.
import { kuratchiDO } from '@kuratchi/js';
import { autoMigrate, kuratchiORM } from '@kuratchi/orm';
import { orgSchema } from './schemas/org';
export default class OrgAuth extends kuratchiDO {
static binding = 'ORG_DB';
declare ctx: DurableObjectState;
declare env: any;
constructor(ctx: DurableObjectState, env: any) {
super();
this.ctx = ctx;
this.env = env;
autoMigrate(ctx.storage, orgSchema);
this.db = kuratchiORM(ctx.storage.sql, orgSchema);
}
async createUser(input) { /* implements the auth contract */ }
async getUserByEmail(email) { /* ... */ }
async getUserById(id) { /* ... */ }
async createSession(input) { /* ... */ }
async getSession(tokenHash) { /* ... */ }
async deleteSession(tokenHash) { /* ... */ }
async markEmailVerified(userId) { /* ... */ }
async completeInvite({ userId, passwordHash, name }) { /* ... */ }
}The framework auto-discovers *.do.ts files and adds the matching binding + SQLite migration to wrangler.jsonc.
Secrets
Set auth-related secrets via Worker env/secrets (e.g. AUTH_SECRET, OAuth provider secrets, Turnstile secret). The middleware factory reads them from the request's bound env at runtime.
Composing with Cloudflare Access
@kuratchi/auth and Cloudflare Access answer different questions:
- Cloudflare Access = "is this user from my org?" Verified at the edge against your IdP. Lives in
@kuratchi/js/access. @kuratchi/auth= "is this app user signed in?" Verified against your D1 database. Lives here.
For an admin panel where employees log into an app that customers also use, you can compose both:
// src/middleware.ts
import { defineMiddleware } from '@kuratchi/js';
import { requireCloudflareAccess } from '@kuratchi/js/access';
import { kuratchiAuthMiddleware } from '@kuratchi/auth/middleware';
import { authConfig } from '$server/auth-config';
export default defineMiddleware({
// Edge-level identity: only employees with Access policies can hit /admin
access: requireCloudflareAccess({
audience: env.CF_ACCESS_AUD,
teamDomain: 'mycompany.cloudflareaccess.com',
exclude: ['/api/public/*', '/auth/*', '/'],
}),
// App-level identity: customers sign in to the public app via credentials
auth: kuratchiAuthMiddleware(authConfig),
});The two are independent — neither depends on the other.
When to swap this out
Reasonable signals to outgrow @kuratchi/auth:
- You need WebAuthn / passkeys.
- You need federated identity beyond the included OAuth providers (e.g. SAML, OIDC against arbitrary providers).
- You want sessions in something other than D1 (Redis, JWT, KV).
- You need fine-grained permission graphs beyond role → permission lists.
- You need MFA, magic links beyond email, or device-based session pinning.
- Your team prefers a battle-tested third-party for security-critical surface area.
When that day comes: replace the kuratchiAuthMiddleware step with whatever you're swapping to, drop @kuratchi/auth from your deps, and the rest of your app keeps working. That's the whole point of the middleware-only integration — it's the same kind of seam Express / Hono / Next.js give you.
