Back to blog

Updated 8 August 2026

Own Auth in five minutes


Own Auth runs inside your backend. Install the package, apply its Postgres schema, and export one configured auth object from a backend-only module. Your server routes use that object to create accounts, issue revocable sessions, and verify each protected request.

Install the package

Install own-auth in the server application. Own Auth requires Node.js 20 or later.

Terminal
npm install own-auth

Configure Postgres and the token pepper

DATABASE_URL points the migration and runtime to the same Postgres database. OWN_AUTH_TOKEN_PEPPER is a stable server-only secret that protects session tokens, one-time tokens, phone codes, and API keys before their hashes are stored.

.env
DATABASE_URL=postgres://user:password@localhost:5432/app
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secret

Create the database tables

Run the package migration against the configured database. It creates Own Auth's prefixed tables without modifying the application's existing tables. The status command confirms that every package migration has been applied.

Terminal
npx own-auth migrate
npx own-auth status

Create the shared backend auth module

Create one configured auth object in a backend-only module, then import it into the server routes that handle authentication. Do not import this module into browser code because it owns database access and server secrets. session.ttlMs sets a 30-day absolute session lifetime; sign-out and revocation can end the session earlier.

auth.ts
import { createOwnAuth } from "own-auth";

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

Create an account and session

auth.signUpEmailPassword creates the user, hashes the password, and creates a database-backed session in one operation. The backend receives the raw session token once so it can set the application's session cookie. Postgres stores the session record and protected token hash, not the raw credential.

signup.ts
import { auth } from "./auth";

const result = await auth.signUpEmailPassword({
  email: "alice@example.com",
  password: "her-secret-password",
  name: "Alice",
});

const publicUser = {
  id: result.user.id,
  email: result.user.email,
  name: result.user.name,
};

const sessionCookie = {
  value: result.sessionToken,
  expires: result.session.expiresAt,
};

The method applies the configured password policy before creating records. A short password produces weak_password, while an existing address produces email_already_exists. Map these typed AuthError codes to safe application responses instead of exposing database errors.

Direct Own Auth methods return internal user and session records. Select the fields the client needs instead of serializing either record wholesale. The example keeps passwordHash, tokenHash, revocation metadata, and other protected columns inside the backend.

Sign in and handle MFA

auth.signInEmailPassword verifies the stored password hash. A completed sign-in returns a new session. An account with MFA enabled returns mfa_required with the challenge token, available methods, and expiry needed for the second factor. A missing account and an incorrect password both produce invalid_credentials.

signin.ts
import { auth } from "./auth";

export async function signIn(email: string, password: string) {
  const result = await auth.signInEmailPassword({ email, password });

  if (result.status === "mfa_required") {
    return {
      status: result.status,
      challengeToken: result.challengeToken,
      methods: result.methods,
      expiresAt: result.expiresAt,
    };
  }

  return {
    status: result.status,
    user: {
      id: result.user.id,
      email: result.user.email,
      name: result.user.name,
    },
    sessionToken: result.sessionToken,
    sessionExpiresAt: result.session.expiresAt,
  };
}

Transport the session safely

For a same-origin web application, put sessionToken in an HttpOnly, Secure, SameSite=Lax cookie and align its expiry with the session expiry. Native and API clients can send the token as a bearer credential from secure platform storage. Either transport must deliver the token to the backend for verification on every protected request.

Verify every protected request

Read the session token at the server boundary and call auth.getCurrentSession. The method checks the protected token against Postgres and returns null when the session is missing, expired, or revoked. Authorization decisions must use the returned user and session, never an unverified user ID supplied by the client.

protected-route.ts
import { auth } from "./auth";

export async function requireUserId(sessionToken: string) {
  const current = await auth.getCurrentSession(sessionToken);

  if (!current) {
    throw new Error("Unauthorized");
  }

  return current.user.id;
}

Use userId for the route's ownership and permission checks. auth.requireCurrentSession is available when the route should fail immediately without an authenticated session. Both methods read current server state, so expiry, revocation, and disabled users take effect without waiting for a client credential to change.

Revoke before clearing the client

Sign-out has two steps. Call auth.signOut(sessionToken) first so the database session stops working, then clear the cookie or local credential. Clearing only the client credential leaves the server-side session active if the token was copied earlier.

signout.ts
import { auth } from "./auth";

export async function signOut(sessionToken: string) {
  await auth.signOut(sessionToken);
  // Clear the session cookie or client credential after this succeeds.
}

The account now has a revocable session that every protected route verifies against Postgres. Continue with the complete Next.js App Router implementation, AdonisJS controllers and middleware, or custom authentication in a Replit app.