Back to blog

Updated 29 July 2026

Own Auth in five minutes


Install own-auth, connect Postgres, set OWN_AUTH_TOKEN_PEPPER, and create one server-only auth instance. The following setup creates an email-and-password user and verifies its database-backed session.

Install the package

Install own-auth in your backend. Version 0.3.6 requires Node.js 20 or later.

Terminal
npm install own-auth

Configure Postgres and the token pepper

DATABASE_URL selects the Postgres database used by the migration and runtime. Pass OWN_AUTH_TOKEN_PEPPER to createOwnAuth; Own Auth combines it with session tokens, one-time tokens, phone codes, and API keys before storing their protected hashes.

.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 Postgres. It creates Own Auth's 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 one auth instance

Create one auth instance in a server module and import it into authentication routes. Configure password policy, token lifetimes, providers, and sessions on that instance. session.ttlMs below sets a 30-day absolute lifetime; auth.signOut and auth.revokeSession can end it 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 the user and session

auth.signUpEmailPassword creates the user, hashes the password, creates a database-backed session, and returns the raw session token to the backend. The password is never stored as plain text. The raw session token is also not stored by Own Auth; the database contains its protected hash and the session record.

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

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

// Select public fields from user and session for the response.
// Store sessionToken using the application's session transport.
// Use session.expiresAt as the client-side expiry.

auth.signUpEmailPassword applies the configured password policy before creating records. A short password produces the typed weak_password error, while an existing address produces email_already_exists; the route can map those AuthError codes without inspecting database errors.

The engine result contains server records: user includes passwordHash, and session includes tokenHash. Select public fields before serializing either object, or use Own Auth's HTTP handler, whose serializers omit protected columns. Use session.expiresAt, session.idleExpiresAt, session.authenticationMethods, and session.assuranceLevel when the response needs expiry or authentication-state details.

Sign in again

auth.signInEmailPassword verifies the stored password hash. It returns status: "complete" with a session unless the user has MFA enabled, in which case it returns status: "mfa_required" with a challenge token and available methods. A missing account and a wrong password both produce invalid_credentials, while a disabled account produces disabled_user.

signin.ts
const result = await auth.signInEmailPassword({
  email: "alice@example.com",
  password: "her-secret-password",
});

if (result.status === "mfa_required") {
  const challenge = {
    challengeToken: result.challengeToken,
    methods: result.methods,
    expiresAt: result.expiresAt,
  };
} else {
  const { user, session, sessionToken } = result;
}

Transport the session safely

For a same-origin web application, put sessionToken in an HttpOnly, Secure, SameSite=Lax cookie and align its expiry with session.expiresAt. Native and API clients can send the same token as a bearer credential. Both transports still verify the token against the database 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
const current = await auth.getCurrentSession(sessionToken);

if (!current) {
  return new Response("Unauthorized", { status: 401 });
}

const { user, session } = current;

Use auth.requireCurrentSession when the route should fail immediately without an authenticated session. Both methods keep the source of truth on the server, which allows individual sessions to be revoked without waiting for a client-side credential to expire.

Revoke before clearing the client

Signing out has two actions. First call auth.signOut(sessionToken) so the database session stops working. Then clear the cookie or local credential from the client. Clearing only the browser cookie leaves a usable server-side session behind if the token was copied earlier.

signout.ts
await auth.signOut(sessionToken);
// Clear the session cookie or client credential.