{"id":50551844,"url":"https://github.com/manyrows/manyrows-auth","last_synced_at":"2026-06-04T04:02:37.352Z","repository":{"id":359343032,"uuid":"1237626697","full_name":"manyrows/manyrows-auth","owner":"manyrows","description":"Open-source, self-hosted authentication server - run many apps (multi-tenant SSO) from one Go + Postgres deployment. OAuth, passkeys, magic links, password, RBAC, audit logs. A self-hosted Auth0 / Keycloak alternative.","archived":false,"fork":false,"pushed_at":"2026-06-02T23:35:34.000Z","size":1879,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T00:13:15.739Z","etag":null,"topics":["audit-logs","auth","auth0-alternative","authentication","authentication-service","authentication-system","go","golang","keycloak-alternative","login","login-forms","login-screen","login-system","magic-link","oauth","passwordless","postgres","self-hosted","sso","webauthn"],"latest_commit_sha":null,"homepage":"https://manyrows.com","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/manyrows.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T11:02:34.000Z","updated_at":"2026-06-02T23:35:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/manyrows/manyrows-auth","commit_stats":null,"previous_names":["manyrows/manyrows-auth","manyrows/manyrows"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/manyrows/manyrows-auth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manyrows%2Fmanyrows-auth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manyrows%2Fmanyrows-auth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manyrows%2Fmanyrows-auth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manyrows%2Fmanyrows-auth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/manyrows","download_url":"https://codeload.github.com/manyrows/manyrows-auth/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manyrows%2Fmanyrows-auth/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33888302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit-logs","auth","auth0-alternative","authentication","authentication-service","authentication-system","go","golang","keycloak-alternative","login","login-forms","login-screen","login-system","magic-link","oauth","passwordless","postgres","self-hosted","sso","webauthn"],"created_at":"2026-06-04T04:02:36.720Z","updated_at":"2026-06-04T04:02:37.340Z","avatar_url":"https://github.com/manyrows.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ManyRows Auth\n\nSelf-hostable user authentication you can drop in front of your apps.\nSign-in, password reset, email verification, magic links, OAuth (Google,\nApple, Microsoft, GitHub, Kakao, Naver) plus any OIDC/OAuth2 provider, passkeys,\nsessions, audit logs, role-based access - running as a single Go binary\nwith Postgres.\n\nOne install runs many apps. Apps share users through user pools (one\napp or several SSO-style), with their own sign-in settings, OAuth\ncredentials, and roles.\n\n\u003e **Honest status.** ManyRows is built by one developer. It runs\n\u003e in production today - it powers sign-in for\n\u003e [DrumKingdom.com](https://drumkingdom.com)\n\u003e - but there's no QA team, no SLA, and no claim it's\n\u003e bug-free. Run it, kick the tyres, and satisfy yourself it holds up\n\u003e before you put it in front of anything that matters. If something\n\u003e breaks or feels wrong, that's a bug I want to hear about.\n\u003e\n\u003e **Help shape it.** Issues, reproductions, and PRs are genuinely\n\u003e welcome - real-world use is what hardens an auth system. If it\n\u003e almost-but-not-quite fits your case, say so; that feedback moves the\n\u003e roadmap more than anything.\n\u003e\n\u003e **Why open source.** Authentication is security-critical\n\u003e infrastructure - you shouldn't have to trust a black box with your\n\u003e users' credentials. Open source means anyone can audit exactly what\n\u003e the binary does, self-host it with no vendor lock-in, and fork it if\n\u003e I ever step away. AGPL-3.0 keeps it that way; a commercial license is\n\u003e available if those terms don't fit (see *License*).\n\u003e\n\u003e **Your data.** It's your Postgres. Users, sessions, audit logs -\n\u003e query, join, export, or build on them directly in plain SQL. No\n\u003e proprietary API, rate-limited dashboard, or export fee stands\n\u003e between you and the data you own.\n\n---\n\n## Quickstart (Docker)\n\n```bash\ngit clone \u003cthis-repo\u003e\ncd manyrows\ncp .env.example .env       # edit values (especially MANYROWS_FROM_EMAIL)\ndocker compose up -d\n```\n\nOpen `http://localhost:8080`. The first registrant becomes the\nsuper-admin - there's no signup flow after that, so claim it before\nexposing the install.\n\nIf you can't claim before exposure (CI deploys, slow first boot, etc.),\nset `MANYROWS_SUPER_ADMIN_EMAIL=you@yourcompany.com` in `.env` before\n`docker compose up`. The slot is then pre-claimed at boot and only that\nexact email can complete the first registration - random scanners\nhitting the install can't take it.\n\nTo watch the boot:\n\n```bash\ndocker compose logs -f web\n```\n\nTo stop everything (data is preserved in the `manyrows-db` volume):\n\n```bash\ndocker compose down\n```\n\n---\n\n## What you get\n\n- **Sign-in methods** per app: password, OTP code, magic link, OAuth\n  (Google / Apple / Microsoft / GitHub / Kakao / Naver), any OIDC/OAuth2\n  provider, passkeys.\n- **Bring-your-own identity providers** - beyond the six built-in\n  social logins, connect any OpenID Connect or OAuth2 provider per app\n  (Okta, Auth0, Keycloak, Entra/Azure AD, GitLab, Discord, ...) from the\n  admin UI: paste an issuer URL (or explicit endpoints) plus client\n  credentials - no code, no release. Covers corporate SSO and the long\n  tail. PKCE + nonce, signature/issuer/audience verification, and\n  https-only endpoints are enforced for you.\n- **Workspace + project + app hierarchy** - one ManyRows install\n  groups environments (dev / staging / prod) under projects, and\n  projects under workspaces.\n- **Role-based access control** - per-project permissions and roles,\n  default-role assignment on signup.\n- **Session management** - per-app session TTL, cookie-domain control,\n  IP allowlists, CORS origin lists, revocation.\n- **Audit logs** - every authentication event recorded per\n  workspace/app, filterable in the admin AuthLogs view.\n- **Embeddable end-user UI** (`@manyrows/appkit-react`) - drop in a\n  React component, get a fully wired sign-in screen.\n- **OpenID Connect provider** - expose any app over standards-\n  conformant OIDC. Off-the-shelf libraries (next-auth,\n  passport-openidconnect, Spring Security, etc.) integrate by\n  pointing at the per-app discovery URL - no ManyRows SDK required.\n\n---\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/sign-in.png\" width=\"760\" alt=\"End-user sign-in (AppKit)\"\u003e\u003cbr\u003e\u003cbr\u003e\n  \u003cimg src=\"docs/screenshots/admin-dashboard.png\" width=\"760\" alt=\"Admin dashboard\"\u003e\u003cbr\u003e\u003cbr\u003e\n  \u003cimg src=\"docs/screenshots/auth-logs.png\" width=\"760\" alt=\"Auth logs\"\u003e\n\u003c/p\u003e\n\n---\n\n## Configuration\n\nAll knobs are env vars prefixed `MANYROWS_*`. The full list with\ndefaults lives in `.env.example`. The minimum a self-hoster needs to\nset is `MANYROWS_FROM_EMAIL`; everything else has sane defaults.\n\nA few worth knowing:\n\n| Variable                                          | Default | Notes |\n|---------------------------------------------------|---|---|\n| `DATABASE_URL` or `MANYROWS_DATABASE_URL`         | (required) | Postgres connection string. |\n| `MANYROWS_FROM_EMAIL`                             | (none) | Sender address on outbound mail (admin register, password reset, magic links). **Required for production** - the email service refuses to send with an empty From and logs an error. Use an address on your own domain so DKIM/SPF pass. |\n| `MANYROWS_BASE_URL`                               | (auto-pinned) | Pinned automatically on the first `/admin/register`. Set explicitly when behind a known reverse proxy. |\n| `MANYROWS_DB_SCHEMA`                              | `manyrows` | Postgres schema. Override if `manyrows` clashes with anything in the database. |\n| `MANYROWS_SMTP_HOST`/`PORT`/`USERNAME`/`PASSWORD` | (none) | Outbound mail. Without these, mail is logged to stdout. |\n| `MANYROWS_TURNSTILE_ENABLED`                      | `false` | Cloudflare bot challenge on register/login. Off by default. |\n\n### Database tuning\n\nThe pool defaults are fine for most installs. Override these when you know why.\n\n| Variable | Default | Notes |\n|---|---|---|\n| `MANYROWS_POOL_MAX_CONNS` | `20` | Upper bound on the pgxpool. Raise on busy installs; lower behind a connection pooler like PgBouncer. |\n| `MANYROWS_POOL_MIN_CONNS` | (pgx default) | Floor on the pool size. Set when cold-start latency matters. |\n| `MANYROWS_POOL_MIN_IDLE_CONNS` | (pgx default) | Pre-warmed idle connections held ready for bursts. |\n| `MANYROWS_POOL_MAX_CONN_IDLE_TIME_SECONDS` | (pgx default) | Idle pruning. Tighten when your DB charges for connection-minutes. |\n| `MANYROWS_POOL_MAX_CONN_LIFETIME_SECONDS` | (pgx default) | Recycle every connection after this many seconds. Useful behind load balancers that drop long-lived TCP. |\n| `MANYROWS_POOL_HEALTH_CHECK_PERIOD_SECONDS` | (pgx default) | How often pgx pings idle connections to keep them warm. |\n| `MANYROWS_DB_STATEMENT_TIMEOUT_SECONDS` | (server default - usually off) | Postgres `statement_timeout` set on every pooled connection. Bounds the wall-clock any one query can spend before the server cancels it. **Strongly recommend setting this** (start with 30s) - the guardrail against a runaway query pinning a worker forever. |\n| `MANYROWS_DB_CONNECT_TIMEOUT_SECONDS` | (pgx default - wait forever) | TCP+TLS handshake bound on new pool connections. Set when your DB IP can flap during a boot race (Fly, Render) so startup fails loudly instead of hanging. 10s is a sensible value. |\n| `MANYROWS_DB_APPLICATION_NAME` | `manyrows` | Reported via Postgres's `application_name` GUC; visible in `pg_stat_activity` / `pg_stat_statements`. Override per-deploy when one cluster hosts multiple installs (`manyrows-prod`, `manyrows-staging`). |\n| `MANYROWS_DB_SKIP_MIGRATIONS` | `false` | Set to `true` to short-circuit goose on boot. Used by two-step deploys that apply schema separately from the binary rollout - the new binary boots without re-racing migrations the previous deploy already ran. |\n\nAuto-generated on first boot (no setup needed): HMAC keys, encryption\nkey, OTP pepper. They're persisted to `system_secrets` and reused on\nsubsequent boots.\n\n---\n\n## Going to production\n\nManyRows ships as a single static binary with the admin UI and AppKit\nruntime embedded - no sidecars, no asset server - so the production\nstory is short: run it behind a TLS-terminating proxy and point it at\nPostgres. The bundled **Docker Compose** stack, a **standalone\ncontainer**, and **Heroku** are all production-grade paths (see\n[Deployment paths](#deployment-paths) below) - pick whichever matches\nyour infra.\n\nWhichever you pick, do these five things:\n\n1. **Terminate TLS upstream** - Caddy, Traefik, nginx + certbot,\n   Cloudflare proxy, or your platform's load balancer. ManyRows speaks\n   plain HTTP behind the proxy.\n2. **Forward `X-Forwarded-Proto: https`** so cookies get the `Secure`\n   flag and redirect targets are constructed correctly.\n3. **Set `MANYROWS_BASE_URL`** to the canonical hostname before going\n   live (or let the first `/admin/register` pin it from the request).\n4. **Persist `manyrows-db`** - managed Postgres recommended in\n   production. If you stay with the bundled compose Postgres, back the\n   volume up.\n5. **Custom domain + cookie scope** - wire `auth.yourdomain.com` to\n   ManyRows so cookies are first-party with your app. Two per-app\n   settings in the admin UI:\n   - *App → Security → Custom Domain* - set the **Auth domain**\n     (e.g. `auth.drumkingdom.com`). Detailed runbook is on that screen.\n   - *App → Security → Session transport → Enable cookies → Cookie\n     domain* - set this to the **registrable parent domain**\n     (`auth.drumkingdom.com` → `drumkingdom.com`). Skip it and the\n     session cookie is scoped to the auth subdomain only, so it won't\n     be sent on requests from your app's own domain.\n\n### Deployment paths\n\nEvery path below runs the same image and reads the same environment\nvariables ([Configuration](#configuration)); the only real difference is\nwho runs the container and where Postgres lives.\n\n\u003e **Use your own domain for `MANYROWS_BASE_URL`** - a subdomain of your\n\u003e app's registrable domain (`auth.yourdomain.com`), *not* the platform's\n\u003e default host. `*.herokuapp.com`, `*.fly.dev`, and `*.onrender.com` are\n\u003e on the Public Suffix List, so session cookies set there can't be shared\n\u003e first-party with your app - which is the whole point of checklist\n\u003e step 5. Every example below assumes `auth.yourdomain.com`.\n\n#### Docker Compose\n\nThe bundled `docker-compose.yml` is production-capable, not just a local\ndemo. Two ways to take it live:\n\n- **Managed Postgres (recommended).** Drop the `db` service and point\n  `DATABASE_URL` at your managed instance (RDS, Cloud SQL, Neon,\n  Supabase, ...). ManyRows holds no local state, so the `web` service is\n  then stateless and trivially restartable.\n- **Bundled Postgres.** Keep the `db` service for a small single-host\n  install, but change the default `POSTGRES_PASSWORD` (the `.env`\n  default is `manyrows`) and back the `manyrows-db` volume up on a\n  schedule.\n\nEither way: set a real `MANYROWS_FROM_EMAIL` + SMTP credentials, and put\nthe `web` service behind one of the reverse proxies below. It already\nrestarts `unless-stopped`.\n\n#### Standalone container\n\nAny platform that runs an OCI image works - plain `docker run`,\nKubernetes, ECS, Cloud Run, or any orchestrator (Render and Fly.io get\ndedicated recipes below):\n\n```bash\ndocker build -t manyrows .\ndocker run -d -p 8080:8080 \\\n  -e DATABASE_URL=\"postgres://user:pass@host:5432/manyrows?sslmode=require\" \\\n  -e MANYROWS_FROM_EMAIL=\"auth@yourdomain.com\" \\\n  -e MANYROWS_BASE_URL=\"https://auth.yourdomain.com\" \\\n  manyrows\n```\n\nThe binary binds `$PORT` when the platform sets it, falling back to\n`8080` - so most PaaS auto-wire the port with no extra config.\n\n#### Heroku\n\nThe image is Heroku-ready: it honours `$PORT` and defaults to the `prod`\nprofile. Heroku's router terminates TLS and sets `X-Forwarded-Proto`, so\nchecklist steps 1-2 are handled for you either way; the custom-domain\nstep still applies if you front it with `auth.yourdomain.com`.\n\n**Container registry** - simplest, reuses the `Dockerfile`:\n\n```bash\nheroku create your-manyrows\nheroku addons:create heroku-postgresql:essential-0   # provisions DATABASE_URL\nheroku config:set \\\n  MANYROWS_FROM_EMAIL=\"auth@yourdomain.com\" \\\n  MANYROWS_BASE_URL=\"https://auth.yourdomain.com\"\nheroku stack:set container\nheroku container:push web \u0026\u0026 heroku container:release web\n```\n\n**Binary slug via the Platform API** - no Docker; build a Linux binary\nlocally and push it as a slug. It releases to an app you've already\ncreated (run the `heroku create` / `addons:create` / `config:set` steps\nabove first, just skip `stack:set container`), and needs `jq` plus\nHeroku credentials in `~/.netrc` (written by `heroku login`). First\nbuild the slug - the UI bundles come from the committed `build-ui.sh`:\n\n```bash\nbash ./build-ui.sh || { echo \"build-ui failed\"; exit 1; }\n\ncd manyrows-core\nVERSION=$(git describe --tags --always --dirty 2\u003e/dev/null || echo dev)\nGOARCH=amd64 GOOS=linux go build -ldflags=\"-X main.Version=${VERSION}\" \\\n  -o ../app/web start.go\ncd ..\ntar czf slug.tgz ./app   # Heroku expects a top-level ./app dir → /app/web\n```\n\nThen create, upload, and release the slug (set `AppID` to your app):\n\n```bash\nAppID='your-heroku-app'\n\nslug=$(curl -s -X POST \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/vnd.heroku+json; version=3' \\\n  -d '{\"process_types\":{\"web\":\"./web\"}}' \\\n  -n \"https://api.heroku.com/apps/$AppID/slugs\")\n\ncurl -X PUT -H 'Content-Type:' --data-binary @slug.tgz \"$(jq -r '.blob.url' \u003c\u003c\u003c \"$slug\")\"\n\ncurl -X POST \\\n  -H 'Accept: application/vnd.heroku+json; version=3' \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"slug\\\":$(jq '.id' \u003c\u003c\u003c \"$slug\")}\" \\\n  -n \"https://api.heroku.com/apps/$AppID/releases\"\n```\n\n#### Render\n\nRender builds straight from the `Dockerfile`, sets `$PORT`, and\nterminates TLS + forwards `X-Forwarded-Proto` at its edge - so the\nproxy checklist is handled. Commit a `render.yaml` blueprint that\nprovisions Postgres and wires `DATABASE_URL` for you:\n\n```yaml\ndatabases:\n  - name: manyrows-db\n    plan: basic-256mb\n\nservices:\n  - type: web\n    name: manyrows\n    runtime: docker\n    plan: starter\n    healthCheckPath: /health\n    envVars:\n      - key: DATABASE_URL\n        fromDatabase:\n          name: manyrows-db\n          property: connectionString\n      - key: MANYROWS_FROM_EMAIL\n        value: auth@yourdomain.com\n      - key: MANYROWS_BASE_URL\n        sync: false   # set to your custom auth domain (auth.yourdomain.com)\n```\n\nThen *New → Blueprint* in the dashboard and point it at your repo. The\nDockerfile EXPOSEs `8080` and the binary honours `$PORT`, so the port\nwires up with no extra config.\n\n#### Fly.io\n\n`fly launch` reads the `Dockerfile`, picks up its `EXPOSE 8080` as the\n`internal_port`, and writes a `fly.toml` with `force_https = true`. Fly\nterminates TLS and forwards `X-Forwarded-Proto`, so the proxy checklist\nis covered.\n\n```bash\nfly launch --no-deploy              # detects the Dockerfile, writes fly.toml\nfly postgres create                 # or point DATABASE_URL at Supabase/Neon/Fly MPG\nfly postgres attach \u003cpg-app-name\u003e   # sets the DATABASE_URL secret\nfly secrets set MANYROWS_FROM_EMAIL=auth@yourdomain.com \\\n                MANYROWS_BASE_URL=https://auth.yourdomain.com\nfly deploy\n```\n\nThe binary's default port (`8080`) matches the `internal_port` Fly\ndetects, so there's no `$PORT` wiring to do.\n\n### Reverse-proxy examples\n\n#### Caddy\n\nAuto-managed TLS via Let's Encrypt. Drop into `/etc/caddy/Caddyfile`\nand `systemctl reload caddy`:\n\n```caddyfile\nauth.example.com {\n    reverse_proxy localhost:8080\n}\n```\n\nThat's the whole config - Caddy adds `X-Forwarded-For`,\n`X-Forwarded-Proto`, and `X-Forwarded-Host` automatically. If your\nManyRows container is on another host, swap `localhost` for the\ninternal hostname / IP.\n\n#### nginx\n\nBring your own cert (certbot, Let's Encrypt DNS-01, internal CA,\nwhatever). Minimum working config:\n\n```nginx\nserver {\n    listen 80;\n    server_name auth.example.com;\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name auth.example.com;\n\n    ssl_certificate     /etc/letsencrypt/live/auth.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;\n\n    location / {\n        proxy_pass         http://127.0.0.1:8080;\n        proxy_set_header   Host              $host;\n        proxy_set_header   X-Real-IP         $remote_addr;\n        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header   X-Forwarded-Proto $scheme;\n    }\n}\n```\n\n`X-Forwarded-Proto $scheme` is the load-bearing line: without it the\nbinary won't realise requests are HTTPS and the session cookies will\nmiss the `Secure` flag.\n\n### Upgrades and backups\n\n- **Upgrades.** Pull the new image (or push a new slug) and restart.\n  Schema migrations run automatically on boot via goose. For rollouts\n  that apply schema separately from the binary, run migrations once\n  out-of-band and set `MANYROWS_DB_SKIP_MIGRATIONS=true` on the new\n  release so it boots without re-racing them.\n- **Backups.** Managed Postgres gives you automated snapshots - use\n  them. On the bundled compose Postgres, `pg_dump` on a schedule. The\n  auto-generated HMAC/encryption keys and OTP pepper live in the\n  database (`system_secrets`), so a Postgres backup captures everything\n  - there's no separate keystore to save.\n- **Health checks.** Point your platform's liveness/readiness probe at\n  `/health` (it also reports the running build version).\n\n---\n\n## Adding login to your app (AppKit)\n\nYou don't have to build a sign-in screen. ManyRows ships **AppKit** -\na drop-in end-user auth UI (sign-in, registration, OTP verification,\npassword reset, profile) that talks to your install. It's an optional\nconvenience layer for React (with a framework-free runtime too); if you\nwant full control, call the Client REST API directly. Full reference -\nevery prop, hook, theming, auth-route handling, the REST API - is at\n**\u003chttps://manyrows.com/docs\u003e**.\n\n\u003e **CORS - required.** AppKit calls ManyRows from *your* app's origin,\n\u003e so add your domain (e.g. `https://yourapp.com`) to the app's allowed\n\u003e CORS origins in the admin UI (Apps page) - otherwise the browser\n\u003e blocks every request.\n\n**React** - `npm i @manyrows/appkit-react`:\n\n```tsx\nimport { AppKit, AppKitAuthed, useUser } from \"@manyrows/appkit-react\";\n\nfunction MyApp() {\n  const user = useUser();\n  return \u003cp\u003eWelcome, {user?.name || user?.email}\u003c/p\u003e;\n}\n\nexport default function Page() {\n  return (\n    \u003cAppKit\n      workspace=\"your-workspace\"\n      appId=\"your-app-id\"\n      src=\"https://auth.yourdomain.com/appkit/assets/appkit.js\"\n    \u003e\n      \u003cAppKitAuthed fallback={null}\u003e\n        \u003cMyApp /\u003e\n      \u003c/AppKitAuthed\u003e\n    \u003c/AppKit\u003e\n  );\n}\n```\n\nOnly `workspace` and `appId` are required. Because you're self-hosting,\nset the `src` prop to your install's runtime URL - otherwise AppKit\nloads the hosted (manyrows.com) runtime by default.\n\n**Without React** - load the runtime and drive `window.ManyRows.AppKit`:\n\n```html\n\u003cscript src=\"https://auth.yourdomain.com/appkit/assets/appkit.js\" defer\u003e\u003c/script\u003e\n\u003cdiv id=\"manyrows-app\"\u003e\u003c/div\u003e\n\u003cscript\u003e\n  window.addEventListener(\"load\", () =\u003e {\n    window.ManyRows.AppKit.init({\n      containerId: \"manyrows-app\",\n      workspace: \"your-workspace\",\n      appId: \"your-app-id\",\n      onState: (s) =\u003e {\n        if (s.status === \"authenticated\") {\n          console.log(\"user:\", s.appData?.account?.email, \"token:\", s.jwtToken);\n        }\n      },\n    });\n  });\n\u003c/script\u003e\n```\n\nThe runtime is served by your own binary at\n`/appkit/assets/appkit.js` (embedded - nothing extra to deploy).\n\n---\n\n## Integrating via OpenID Connect\n\nIf you'd rather use a standards-conformant OIDC client library than\nthe AppKit SDK, ManyRows exposes each app as an OpenID Connect\nprovider. Discovery, authorize, token, userinfo, and end-session\nendpoints are all built in; PKCE is required, S256 only; both\nconfidential (with `client_secret`) and public (PKCE-only) client\nmodes are supported.\n\nConfigure in *App → Auth methods → OIDC*: flip the toggle, optionally\ngenerate a `client_secret` (shown once - copy it then), and add your\nRP's callback URL to the redirect-URIs allowlist. The admin tab\nsurfaces the three values your RP library needs:\n\n| Field | Value pattern |\n|---|---|\n| Discovery URL | `https://\u003cauth-domain\u003e/.well-known/openid-configuration` |\n| Client ID | The app's UUID |\n| Client Secret | Generated server-side; copy from the dialog once |\n\nPoint any standard OIDC client at the discovery URL and it\nself-configures. Example with `next-auth`:\n\n```ts\nimport { type AuthOptions } from \"next-auth\";\n\nexport const authOptions: AuthOptions = {\n  providers: [\n    {\n      id: \"manyrows\",\n      name: \"ManyRows\",\n      type: \"oauth\",\n      wellKnown: \"https://auth.yourdomain.com/.well-known/openid-configuration\",\n      clientId: process.env.MANYROWS_CLIENT_ID,      // the app UUID\n      clientSecret: process.env.MANYROWS_CLIENT_SECRET,\n      authorization: { params: { scope: \"openid email\" } },\n    },\n  ],\n};\n```\n\n\u003e **Cookie transport mode required.** OIDC's `/authorize` → sign-in\n\u003e → `/authorize/resume` round-trip relies on a same-origin session\n\u003e cookie. Switch the app's *Session transport* to cookies before\n\u003e enabling OIDC; the admin UI blocks the enable toggle when it isn't.\n\nCoexists with the AppKit SDK - both can authenticate against the\nsame app in parallel.\n\n---\n\n## Architecture (one paragraph)\n\nSingle Go binary (`manyrows-core`) with the admin UI bundle and the\nend-user auth UI bundle compiled in via `//go:embed`. Postgres is the\nonly external dependency - schema lives in `manyrows-core/db/migrations`,\napplied at boot via `goose` into a configurable schema (`manyrows` by\ndefault). Admin auth uses cookie sessions; end-user auth issues\nJWT bearer tokens (`local` transport) or HttpOnly cookies (`cookie`\ntransport), selectable per app.\n\n---\n\n## Design notes\n\nThe *why* behind the non-obvious decisions - password hashing, DPoP-bound\nrefresh tokens, verified-email account linking, secrets-at-rest, and the\n\"standard\" features deliberately left out - is written up in\n[`docs/design-notes.md`](docs/design-notes.md).\n\n---\n\n## Development\n\n```bash\n# Run from source (dev mode, hot reload UI):\ncd manyrows-ui \u0026\u0026 npm install \u0026\u0026 npm run dev   # in one terminal\ncd manyrows-core \u0026\u0026 go run start.go            # in another\n\n# Run all API tests (needs a dedicated test database):\nexport TEST_DATABASE_URL=\"postgres://postgres:postgres@localhost:5432/manyrows_test\"\ncd manyrows-core\ngo test ./api/... -count=1\n\n# Run a specific test:\ngo test -v ./api/... -run \"TestCreateProject\" -count=1\n```\n\nThe repo is an npm workspace at the root, so `npm install` from the\ntop-level pulls deps for `manyrows-ui` and `appkit-ui` in one shot.\n`appkit-react` (the published customer SDK) is standalone - it's not\npart of the workspace and isn't needed to build or run the server;\ninstall its deps separately when working on it.\n\n---\n\n## License\n\n[GNU Affero General Public License v3.0](./LICENSE) (AGPL-3.0).\n\nYou can self-host, modify, and redistribute the code freely. If you\nrun a modified version as a network service, you must publish your\nchanges under AGPL-3.0 too - that's the SaaS-loophole-closing clause\nspecific to AGPL.\n\nA commercial license is available on request for organisations that\ncan't ship under AGPL terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanyrows%2Fmanyrows-auth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmanyrows%2Fmanyrows-auth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanyrows%2Fmanyrows-auth/lists"}