Logto is an open-source Auth0 alternative providing self-hosted OIDC, multi-tenant organizations, and enterprise SSO backed by PostgreSQL.
Auth0 bills teams per active user and locks credential stores inside a proprietary cloud. Logto delivers OpenID Connect authentication, multi-tenant organization tenancy, and enterprise single sign-on from containers running directly within your own network perimeter.
Auth0 operates as a managed black box where user records and session states live off-premises. Logto provides the same core architecture—OpenID Connect, OAuth 2.1, role-based access control, machine-to-machine tokens, and SAML federation—while storing records in a standard PostgreSQL database you control. Engineering teams interact with an administrative console or automate provisioning through a REST Management API, running database migrations locally through the Logto CLI.
The operational trade-off centers on infrastructure ownership. Auth0 eliminates maintenance overhead by managing high availability, compliance, and patch cycles at the cost of steep usage-tier pricing. Operating Logto means your team maintains database replication, backups, and upgrades, with an optional Redis cache for token lookups. For engineering groups subject to strict data residency mandates or fast-growing user counts, running an in-house identity plane removes external dependencies and unpredictable subscription invoices.
Logto vs Auth0 — which should engineering teams choose? Auth0 fits organizations that need turnkey compliance certifications and managed global availability with zero infrastructure management. Logto is the better option for teams that want to self-host their identity store on PostgreSQL, avoid per-active-user cloud fees, and retain full code and data ownership.
Is Logto free for commercial use? Yes. The open-source edition is published under the Mozilla Public License 2.0 (MPL-2.0), which permits commercial deployment and modification provided modified source files remain under the same license.
Can Logto run in air-gapped or private environments? Yes. Logto packages into Docker containers and connects to PostgreSQL and optional Redis instances entirely within isolated private networks or Kubernetes clusters.
Logto is a strong choice for engineering teams that need production-ready B2B authentication without vendor lock-in or escalating per-user pricing, provided they are prepared to manage their own PostgreSQL infrastructure and upgrade procedures. Active maintenance with commits through September 2026 and an MPL-2.0 license—which protects source modifications while leaving your proprietary application code untouched—make it a dependable foundation for self-hosted identity systems.
We started this exact setup from scratch in a clean Linux machine on Nov 1, 2026 and checked it through Logto's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p logto && cd logto
curl -fsSL https://open-source.store/tools/logto/compose.yml -o compose.ymlcat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 12)
ENDPOINT=http://localhost:3001
ADMIN_ENDPOINT=http://localhost:3002
EOFPOSTGRES_PASSWORD — Password for the bundled PostgreSQL database user. Generated above; keep it secret.ENDPOINT — Public URL for the Logto core service (OIDC issuer); update with your public domain for production.ADMIN_ENDPOINT — Public URL for the Logto Admin Console; update with your public domain for production.docker compose up -dservices:
app:
image: svhd/logto:1.45.0
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
ports:
- "3001:3001"
- "3002:3002"
environment:
TRUST_PROXY_HEADER: "1"
DB_URL: postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/logto
ENDPOINT: ${ENDPOINT}
ADMIN_ENDPOINT: ${ADMIN_ENDPOINT}
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3001/oidc/.well-known/openid-configuration').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
postgres:
image: postgres:17-alpine
user: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: logto
volumes:
- logto-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d logto"]
interval: 5s
timeout: 5s
retries: 5
volumes:
logto-db-data:
Go, gRPC and Protobuf done right, LLM agents in production, and the hard parts of shipping a first product.
Show that Logto is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 53 s: postgres:17-alpine, svhd/logto:1.45.0.
Based on the upstream setup. Official compose. Image pinned to 1.45.0 and postgres pinned to 17-alpine; database password and endpoints moved to .env; persistent named volume and healthcheck added.
app-1 | 2026-11-01T03:47:58.459794173Z srRCKeZxBs74hza3 --> GET /lib/auth.json 200 14ms - app-1 | 2026-11-01T03:47:58.459828590Z 4MyFCR0mtyqO9lMP --> GET /lib/auth.json 200 16ms - app-1 | 2026-11-01T03:47:58.460399381Z ST5-20oEmy9skLIF <-- GET /libs/auth.json app-1 | 2026-11-01T03:47:58.462207506Z NsvVMfkVEQky3NL- <-- GET /libs/auth.json app-1 | 2026-11-01T03:47:58.466113465Z gvhXqzXLix3HL37i --> GET /includes/auth.json 200 20ms - app-1 | 2026-11-01T03:47:58.466829048Z lErjPRknDsODEOy0 <-- GET /include/auth.json app-1 | 2026-11-01T03:47:58.479547423Z oLiKV16bLTwHPOiS --> GET /libs/auth.json 200 27ms - app-1 | 2026-11-01T03:47:58.479556173Z ST5-20oEmy9skLIF --> GET /libs/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.479557048Z NsvVMfkVEQky3NL- --> GET /libs/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.479557590Z DjRm37LOD3N-E6pp <-- GET /library/auth.json app-1 | 2026-11-01T03:47:58.480256340Z r70_B7GcB_MDe_2q <-- GET /library/auth.json app-1 | 2026-11-01T03:47:58.481587756Z AbvYDsEstgK3F4l- <-- GET /library/auth.json app-1 | 2026-11-01T03:47:58.486092423Z lErjPRknDsODEOy0 --> GET /include/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.487229465Z B8jKy8sy67EXtOQb <-- GET /lib/auth.json app-1 | 2026-11-01T03:47:58.497151506Z r70_B7GcB_MDe_2q --> GET /library/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.497630548Z DjRm37LOD3N-E6pp --> GET /library/auth.json 200 18ms - app-1 | 2026-11-01T03:47:58.497632840Z oH3vPdE-54sq43w4 <-- GET /vendor/auth.json app-1 | 2026-11-01T03:47:58.497747256Z 9YgWjmPAhwFKGK8K <-- GET /vendor/auth.json app-1 | 2026-11-01T03:47:58.498882798Z AbvYDsEstgK3F4l- --> GET /library/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.499366256Z rDU2AEEiCeuFBL4S <-- GET /vendor/auth.json app-1 | 2026-11-01T03:47:58.505642006Z B8jKy8sy67EXtOQb --> GET /lib/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.508004715Z ofb1Be0p9StFBjw0 <-- GET /libs/auth.json app-1 | 2026-11-01T03:47:58.511236006Z 9YgWjmPAhwFKGK8K --> GET /vendor/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.511240131Z oH3vPdE-54sq43w4 --> GET /vendor/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.511240881Z cpvCvh6zE4Dr42rV <-- GET /vendors/auth.json app-1 | 2026-11-01T03:47:58.512759881Z tbm507lW8xz61Omo <-- GET /vendors/auth.json app-1 | 2026-11-01T03:47:58.514384881Z rDU2AEEiCeuFBL4S --> GET /vendor/auth.json 200 15ms - app-1 | 2026-11-01T03:47:58.515581173Z weEwHwTztKulc0Nv <-- GET /vendors/auth.json app-1 | 2026-11-01T03:47:58.523628673Z cpvCvh6zE4Dr42rV --> GET /vendors/auth.json 200 12ms - app-1 | 2026-11-01T03:47:58.523711798Z tbm507lW8xz61Omo --> GET /vendors/auth.json 200 11ms - app-1 | 2026-11-01T03:47:58.523954381Z ofb1Be0p9StFBjw0 --> GET /libs/auth.json 200 16ms - app-1 | 2026-11-01T03:47:58.525204381Z PEYlaOf9vRllYpVO <-- GET /node_modules/auth.json app-1 | 2026-11-01T03:47:58.526756215Z is-KUWtp7gICXvpq <-- GET /node_modules/auth.json app-1 | 2026-11-01T03:47:58.527191798Z w1hNkUtJGpb1oqNb <-- GET /library/auth.json app-1 | 2026-11-01T03:47:58.528474590Z weEwHwTztKulc0Nv --> GET /vendors/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.530038381Z DSmsXk-IsfeA1Eye <-- GET /node_modules/auth.json app-1 | 2026-11-01T03:47:58.538847881Z is-KUWtp7gICXvpq --> GET /node_modules/auth.json 200 12ms - app-1 | 2026-11-01T03:47:58.539142840Z IDLYsFfC_n8TMGTT <-- GET /storage/auth.json app-1 | 2026-11-01T03:47:58.545902381Z DSmsXk-IsfeA1Eye --> GET /node_modules/auth.json 200 15ms - app-1 | 2026-11-01T03:47:58.549169298Z KJ5a7GesdDiyOsep <-- GET /storage/auth.json app-1 | 2026-11-01T03:47:58.549380381Z w1hNkUtJGpb1oqNb --> GET /library/auth.json 200 22ms - app-1 | 2026-11-01T03:47:58.551369506Z PEYlaOf9vRllYpVO --> GET /node_modules/auth.json 200 25ms - app-1 | 2026-11-01T03:47:58.551372673Z egjHk3RHregiUF3t <-- GET /vendor/auth.json app-1 | 2026-11-01T03:47:58.551813131Z ZPxQu-BIH106dT14 <-- GET /storage/auth.json app-1 | 2026-11-01T03:47:58.554340298Z IDLYsFfC_n8TMGTT --> GET /storage/auth.json 200 15ms - app-1 | 2026-11-01T03:47:58.554794590Z q3eAW5ziKOvs1ljz <-- GET /database/auth.json app-1 | 2026-11-01T03:47:58.566529715Z KJ5a7GesdDiyOsep --> GET /storage/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.566948756Z OVlnOj88lg5nKvtc <-- GET /database/auth.json app-1 | 2026-11-01T03:47:58.567137381Z egjHk3RHregiUF3t --> GET /vendor/auth.json 200 16ms - app-1 | 2026-11-01T03:47:58.567508048Z ee9MplcaMti-nhNc <-- GET /vendors/auth.json app-1 | 2026-11-01T03:47:58.571687590Z q3eAW5ziKOvs1ljz --> GET /database/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.571692590Z RXXS8yn4Om5KaPAS <-- GET /db/auth.json app-1 | 2026-11-01T03:47:58.573256756Z ZPxQu-BIH106dT14 --> GET /storage/auth.json 200 22ms - app-1 | 2026-11-01T03:47:58.577325173Z FzCyzhYyPb6paQ8- <-- GET /database/auth.json app-1 | 2026-11-01T03:47:58.580650381Z RXXS8yn4Om5KaPAS --> GET /db/auth.json 200 9ms - app-1 | 2026-11-01T03:47:58.582359423Z T1ZWc6ry_JopsLyD <-- GET /auth/auth.json app-1 | 2026-11-01T03:47:58.587959215Z ee9MplcaMti-nhNc --> GET /vendors/auth.json 200 20ms - app-1 | 2026-11-01T03:47:58.589678340Z L7q_927ot-qtfVAT <-- GET /node_modules/auth.json app-1 | 2026-11-01T03:47:58.593561881Z OVlnOj88lg5nKvtc --> GET /database/auth.json 200 27ms - app-1 | 2026-11-01T03:47:58.594412840Z pWFlMiW6pILi88P3 <-- GET /db/auth.json app-1 | 2026-11-01T03:47:58.597641673Z FzCyzhYyPb6paQ8- --> GET /database/auth.json 200 22ms - app-1 | 2026-11-01T03:47:58.597690590Z OUt4kwc5AfVUHlEf <-- GET /db/auth.json app-1 | 2026-11-01T03:47:58.606781465Z L7q_927ot-qtfVAT --> GET /node_modules/auth.json 200 17ms - app-1 | 2026-11-01T03:47:58.608511048Z wyKOr92OreTYl3az <-- GET /storage/auth.json app-1 | 2026-11-01T03:47:58.613025756Z pWFlMiW6pILi88P3 --> GET /db/auth.json 200 18ms - app-1 | 2026-11-01T03:47:58.613032298Z T1ZWc6ry_JopsLyD --> GET /auth/auth.json 200 30ms - app-1 | 2026-11-01T03:47:58.613033131Z OUt4kwc5AfVUHlEf --> GET /db/auth.json 200 15ms - app-1 | 2026-11-01T03:47:58.613033715Z TOwf6_Ds6kxgSmZz <-- GET /auth/auth.json app-1 | 2026-11-01T03:47:58.614160631Z lolzY1l58YcELqrY <-- GET /authentication/auth.json app-1 | 2026-11-01T03:47:58.614334173Z fvYkd_t45NQi5qNn <-- GET /auth/auth.json app-1 | 2026-11-01T03:47:58.622508423Z TOwf6_Ds6kxgSmZz --> GET /auth/auth.json 200 10ms - app-1 | 2026-11-01T03:47:58.623147715Z wyKOr92OreTYl3az --> GET /storage/auth.json 200 15ms - app-1 | 2026-11-01T03:47:58.624027006Z 7JXnwEOE9jeG_g6i <-- GET /authentication/auth.json app-1 | 2026-11-01T03:47:58.624679881Z m-UAQdI9bp1XPXTr <-- GET /database/auth.json app-1 | 2026-11-01T03:47:58.627429298Z lolzY1l58YcELqrY --> GET /authentication/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.627619090Z fvYkd_t45NQi5qNn --> GET /auth/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.628098131Z AleNcPGmM3_di6od <-- GET /oauth/auth.json app-1 | 2026-11-01T03:47:58.628185215Z la-P3A6a1_O0W7DH <-- GET /authentication/auth.json app-1 | 2026-11-01T03:47:58.634433756Z 7JXnwEOE9jeG_g6i --> GET /authentication/auth.json 200 11ms - app-1 | 2026-11-01T03:47:58.635114840Z rvyJKRFS1G7em212 <-- GET /oauth/auth.json app-1 | 2026-11-01T03:47:58.637397381Z AleNcPGmM3_di6od --> GET /oauth/auth.json 200 9ms - app-1 | 2026-11-01T03:47:58.637873506Z pJaDd83SCG94s73a <-- GET /keys/auth.json app-1 | 2026-11-01T03:47:58.648367340Z la-P3A6a1_O0W7DH --> GET /authentication/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.648372423Z smblF-GD6TAWFfjW <-- GET /oauth/auth.json app-1 | 2026-11-01T03:47:58.648875756Z m-UAQdI9bp1XPXTr --> GET /database/auth.json 200 24ms - app-1 | 2026-11-01T03:47:58.648935215Z nzCDXwy43p8TSjWl <-- GET /db/auth.json app-1 | 2026-11-01T03:47:58.654725881Z rvyJKRFS1G7em212 --> GET /oauth/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.656459256Z 8vR09uNYeAEkCfI4 <-- GET /keys/auth.json app-1 | 2026-11-01T03:47:58.662903215Z smblF-GD6TAWFfjW --> GET /oauth/auth.json 200 14ms - app-1 | 2026-11-01T03:47:58.662908131Z nzCDXwy43p8TSjWl --> GET /db/auth.json 200 14ms - app-1 | 2026-11-01T03:47:58.662908756Z 4NVAGzIDApnq1_wO <-- GET /keys/auth.json app-1 | 2026-11-01T03:47:58.663390423Z oiAp_SNJTsRtXFzc <-- GET /auth/auth.json app-1 | 2026-11-01T03:47:58.669461923Z pJaDd83SCG94s73a --> GET /keys/auth.json 200 32ms - app-1 | 2026-11-01T03:47:58.674999465Z 8vR09uNYeAEkCfI4 --> GET /keys/auth.json 200 13ms - app-1 | 2026-11-01T03:47:58.675006131Z C_nfW1BaS_UU355- <-- GET /credentials/auth.json app-1 | 2026-11-01T03:47:58.675007006Z d8X-1_XQoU8yzbBC <-- GET /credentials/auth.json app-1 | 2026-11-01T03:47:58.685231090Z C_nfW1BaS_UU355- --> GET /credentials/auth.json 200 11ms - app-1 | 2026-11-01T03:47:58.685739006Z 4NVAGzIDApnq1_wO --> GET /keys/auth.json 200 23ms - app-1 | 2026-11-01T03:47:58.688094756Z oiAp_SNJTsRtXFzc --> GET /auth/auth.json 200 25ms - app-1 | 2026-11-01T03:47:58.690668631Z JMyz0RrJumHD_fsj <-- GET /creds/auth.json app-1 | 2026-11-01T03:47:58.692954048Z d8X-1_XQoU8yzbBC --> GET /credentials/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.696295340Z c0Dkz7-jT00Yuymr <-- GET /authentication/auth.json app-1 | 2026-11-01T03:47:58.697101923Z a0X18dQ1ZyazzAMW <-- GET /credentials/auth.json app-1 | 2026-11-01T03:47:58.697555590Z Vg8IPma5OEnWyzBL <-- GET /creds/auth.json app-1 | 2026-11-01T03:47:58.708930673Z c0Dkz7-jT00Yuymr --> GET /authentication/auth.json 200 14ms - app-1 | 2026-11-01T03:47:58.709392798Z JMyz0RrJumHD_fsj --> GET /creds/auth.json 200 19ms - app-1 | 2026-11-01T03:47:58.711633381Z Vg8IPma5OEnWyzBL --> GET /creds/auth.json 200 14ms - app-1 | 2026-11-01T03:47:58.713684840Z a0X18dQ1ZyazzAMW --> GET /credentials/auth.json 200 16ms - app-1 | 2026-11-01T03:47:58.713919340Z kzwecCNh3IHaFci4 <-- GET /oauth/auth.json app-1 | 2026-11-01T03:47:58.715598715Z 0HaiRuEYC5DSTyFB <-- GET /creds/auth.json app-1 | 2026-11-01T03:47:58.720225506Z kzwecCNh3IHaFci4 --> GET /oauth/auth.json 200 7ms - app-1 | 2026-11-01T03:47:58.722829423Z GguhviTe8Kxk7wjA <-- GET /keys/auth.json app-1 | 2026-11-01T03:47:58.725835673Z 0HaiRuEYC5DSTyFB --> GET /creds/auth.json 200 10ms - app-1 | 2026-11-01T03:47:58.731567965Z GguhviTe8Kxk7wjA --> GET /keys/auth.json 200 9ms - app-1 | 2026-11-01T03:47:58.732323423Z a1NLo-eN8tP1akTo <-- GET /credentials/auth.json app-1 | 2026-11-01T03:47:58.738964506Z a1NLo-eN8tP1akTo --> GET /credentials/auth.json 200 6ms - app-1 | 2026-11-01T03:47:58.739662798Z mHXOFiDjUcdfKk4b <-- GET /creds/auth.json app-1 | 2026-11-01T03:47:58.746250923Z mHXOFiDjUcdfKk4b --> GET /creds/auth.json 200 7ms - app-1 | 2026-11-01T03:47:58.968803548Z M7JuaDnVZo1XlMzm <-- GET /oidc/.well-known/openid-configuration app-1 | 2026-11-01T03:47:58.970350840Z M7JuaDnVZo1XlMzm --> GET /oidc/.well-known/openid-configuration 200 2ms -