Getting Started

Free-IdP Hosting Guide

Run your own persistent OpenApe IdP with Drizzle + libsql.

Free-IdP Hosting Guide

This guide walks through standing up a persistent, self-hosted OpenApe IdP using the reference openape-free-idp app — a Nuxt 4 project that wires @openape/nuxt-auth-idp to a libsql database (either a local SQLite file or Turso).

If you only need a dev sandbox or want to bring your own persistence layer, see Quick Start for the minimal path.

What you get

  • Passkey login (WebAuthn) and SSH-key agent auth out of the box.
  • OAuth/OIDC endpoints (/authorize, /token, /.well-known/openid-configuration, /userinfo, JWKS).
  • Grant approval flows at /grants, /grant-approval, /enroll.
  • Admin UI at /admin for users, agents, sessions, and registration URLs.
  • Optional multi-tenant hosting — one instance, many domains.
  • A working schema with migrations, backfills, and indexes you can iterate on.

Prerequisites

  • Node 22+
  • pnpm 10+
  • A libsql target. Either:
    • Local file (recommended for development and single-node self-hosting) — any path on disk
    • Turso (recommended for HA or multi-region) — a free-tier Turso DB works

Setup

1. Clone the reference app

git clone https://github.com/openape-ai/openape
cd openape
pnpm install
cd apps/openape-free-idp

2. Configure the database

The database is accessed through @libsql/clientdrizzle-orm. useDb() reads tursoUrl and tursoAuthToken from runtimeConfig:

// apps/openape-free-idp/server/database/drizzle.ts
import { createClient } from '@libsql/client'
import { drizzle } from 'drizzle-orm/libsql'

export function useDb() {
  const config = useRuntimeConfig()
  const client = createClient({
    url: config.tursoUrl as string,         // file:./data/idp.db  OR  libsql://...
    authToken: config.tursoAuthToken as string, // empty for file:// URLs
  })
  return drizzle(client, { schema })
}

Option A — local SQLite file

export NUXT_TURSO_URL="file:./data/idp.db"
export NUXT_TURSO_AUTH_TOKEN=""

The DB file is created on first startup. The startup plugin server/plugins/02.database.ts runs idempotent CREATE TABLE IF NOT EXISTS statements for every table the module needs.

Option B — Turso

turso db create my-idp
turso db tokens create my-idp
export NUXT_TURSO_URL="libsql://my-idp-YOURORG.turso.io"
export NUXT_TURSO_AUTH_TOKEN="eyJhbGciOi…"

3. Configure the IdP module

apps/openape-free-idp/nuxt.config.ts already includes the module. Set the runtime secrets:

export NUXT_OPENAPE_IDP_SESSION_SECRET=$(openssl rand -hex 32)
export NUXT_OPENAPE_IDP_MANAGEMENT_TOKEN=$(openssl rand -hex 32)
export NUXT_OPENAPE_IDP_ADMIN_EMAILS="you@example.com"

For a single-hostname deployment, also set:

export NUXT_OPENAPE_IDP_RP_ID=id.example.com
export NUXT_OPENAPE_IDP_RP_ORIGIN=https://id.example.com
export NUXT_OPENAPE_IDP_ISSUER=https://id.example.com

For multi-tenant, set rpHostAllowList instead and leave the static RP fields empty:

export NUXT_OPENAPE_IDP_RP_HOST_ALLOW_LIST="id.example.com,id.example.at"

4. Start it

pnpm dev    # http://localhost:3003

On first request the database tables and indexes are created. The login page is at /login; the admin UI at /admin (accessible to emails in NUXT_OPENAPE_IDP_ADMIN_EMAILS).

5. Create your first user

Use the management token to mint a registration URL:

curl -X POST http://localhost:3003/api/admin/registration-urls \
  -H "Authorization: Bearer $NUXT_OPENAPE_IDP_MANAGEMENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","name":"You"}'

Open the returned URL and register a passkey.

Production build

pnpm --filter openape-free-idp build
node .output/server/index.mjs

Put nginx (or another reverse proxy) in front and terminate TLS there. A minimal vhost:

server {
  listen 443 ssl http2;
  server_name id.example.com;

  ssl_certificate     /etc/letsencrypt/live/id.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/id.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:3003;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
  }
}

The runtime is Node 22+ — the same binary works under systemd, a container, or any Node-capable host.

Migrations

The reference app ships two parallel mechanisms:

  • Drizzle-kit migrations under server/database/migrations/ — used in dev to track schema history and generate DDL.
  • Idempotent startup init in server/plugins/02.database.ts — the authoritative path in production. CREATE TABLE IF NOT EXISTS + ALTER TABLE ADD COLUMN guarded by PRAGMA table_info lookups. Safe to run on every boot; handles new deploys and schema evolution without a separate migration step.

When you add a schema change, update both: write the Drizzle migration for version control, and mirror the DDL in 02.database.ts so fresh nodes come up cleanly.

Customizing

Every store can be swapped by registering a different factory in server/plugins/04.idp-stores.ts:

import { defineUserStore, defineCredentialStore,} from '#imports'

export default defineNitroPlugin(() => {
  defineUserStore(() => myPostgresUserStore)
  defineCredentialStore(() => myPostgresCredentialStore)
  // …
})

Common reasons to replace stores:

  • Swap libsql for Postgres / DynamoDB / MongoDB.
  • Add caching in front of the credential lookup.
  • Plug into an existing identity provider during migration.

The CredentialStore, UserStore, CodeStore, etc. interfaces are exported from @openape/auth; implementing each is a few methods per store.