← Back to blog

How to structure Own Auth in a TypeScript monorepo


A TypeScript monorepo should have one package that creates the Own Auth server instance. That package owns database access, secrets, migrations, provider credentials, and every call that can create or verify a session. Web applications, native applications, and background services should not each construct a slightly different authentication instance.

Give server authentication one owner

Put the server instance in a package that can run only in trusted backend processes. Keep DATABASE_URL, OWN_AUTH_TOKEN_PEPPER, OAuth secrets, encryption keys, email providers, and role definitions there. Run npx own-auth migrate once in the release process before any application version that expects the new schema receives traffic.

packages/auth-server/src/auth.ts
import "server-only";
import { createOwnAuth } from "own-auth";

export const auth = createOwnAuth({
  baseUrl: process.env.APP_URL!,
  tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER!,
  session: {
    ttlMs: 30 * 24 * 60 * 60 * 1000,
    idleTtlMs: 7 * 24 * 60 * 60 * 1000,
  },
});

The server-only guard prevents a Next.js client bundle from importing this file. Other runtimes can enforce the same rule with package exports and lint restrictions. The important result is that no browser package can reach the database adapter or read a server secret.

Export the handler separately

Expose the framework neutral handler from another server entry point. Each backend mounts that handler at one stable path. An application that does not own the authentication origin should call the owning backend instead of importing the auth instance and connecting to the auth tables itself.

packages/auth-server/src/handler.ts
import "server-only";
import { createOwnAuthHandler } from "own-auth/http";

import { auth } from "./auth";

export const authHandler = createOwnAuthHandler(auth, {
  trustedOrigins: ["https://app.example.com"],
});

Keep the browser package public

A separate browser package may export the Own Auth client and application safe types. It must not reexport the server instance, environment helpers, database records, or provider configuration. The client talks to the mounted HTTP handler and receives only the public session shape.

packages/auth-client/src/index.ts
import { createOwnAuthClient } from "own-auth/client";

export const authClient = createOwnAuthClient({
  baseURL: "https://api.example.com/api/auth",
});

export type SignedInUser = {
  id: string;
  email: string | null;
  name: string | null;
  imageUrl: string | null;
};

Package exports should make the separation visible. Export @example/auth-server/auth and @example/auth-server/handler only to backend applications. Export @example/auth-client to browser code. Do not create one barrel file that exports both sides because an accidental client import can pull server dependencies into a build or disclose which environment values exist.

Verify sessions at the application boundary

A backend that shares the server package can call auth.getCurrentSession with the presented session token. The result must be narrowed before application data is loaded. Select the safe fields the caller needs. Never serialize the complete internal user or session record because those records contain protected authentication fields and revocation metadata.

apps/api/src/current-user.ts
import { auth } from "@example/auth-server/auth";

export async function currentUser(sessionToken: string) {
  const current = await auth.getCurrentSession(sessionToken);
  if (!current) return null;

  return {
    id: current.user.id,
    email: current.user.email,
    name: current.user.name,
    imageUrl: current.user.imageUrl,
  };
}

Choose the boundary for another service

A service inside the same trusted backend can receive an already verified actor from the application API. A separately deployed resource server should not receive database credentials merely to validate a browser session. Use the Own Auth authorization server when another application or service needs independently scoped access tokens, resource audiences, token introspection, or consent. That protocol boundary is more explicit than sharing the core auth database across every service.

Prevent version and configuration drift

Pin one Own Auth version in the workspace lockfile. Do not let each application declare an unrelated range that can resolve to a different public contract. A single workspace dependency makes the generated HTTP client, server handler, migrations, and TypeScript types move together. Review the package changelog before updating the shared version.

Every process that constructs or calls the server instance must agree on session limits, provider names, role definitions, passkey origins, and enabled protocol features. Put those values in the server package and pass environment specific origins or credentials through validated configuration. Do not copy an auth configuration into an application folder for convenience.

Deploy migrations before consumers

The release pipeline should treat the authentication package as one deployable dependency even when several applications use it. Build every consumer against the same lockfile, apply Own Auth migrations once, deploy the authentication owner, then release consumers that depend on the new contract. A consumer should not be the first process to discover that the database schema is older than its client types.

If an upgrade changes an HTTP response, publish the server and client compatibility window explicitly. Prefer additive responses that an older client can ignore. When a breaking transition is unavoidable, deploy a server version that accepts both request forms, update every client, then remove the old form in a later release. Keep that transition bounded and tested instead of preserving an undocumented fallback indefinitely.

Keep jobs and scripts on the server side

Migration commands, account administration scripts, delivery workers, and scheduled cleanup jobs import the server package. They should receive the same validated environment as the main backend and close the Own Auth instance before exit. Browser applications and shared UI packages import only public types or the browser client package.

Test the package boundary as part of the build. A browser application should import only the public client entry point. Every server should use the same Own Auth version and authorization definition. The release pipeline should apply migrations once, start the auth owner, verify one complete sign in, and then verify that each consuming service rejects a missing, expired, or revoked credential.