This repository is the Seamless Auth command-line tool (published as seamless-cli, invoked as
seamless or npx create-seamless). It does two things:
- Scaffold a working Seamless Auth project (
seamless init): generates a React frontend, an Express adapter, the auth server, a Docker Compose file, and config. - Verify the whole auth surface (
seamless verify): a cross-package conformance harness that runs an api / adapter / react matrix against the ecosystem.
Use this file as the fast path. The verify harness has its own moving parts under verify/ (a Docker Compose stack plus a Playwright harness).
These rules apply to every repository in the fells-code org. Repo-specific guidance may extend them but must not contradict them.
- Commit and open PRs solely under the repository owner's identity. Never commit under an agent or assistant identity.
- Never attribute work to an AI assistant: no
Co-Authored-By: Claude(or any assistant) trailers, no "Generated with" / "Created with Claude" notes, and no assistant branding or emoji anywhere in commit messages, PR or issue titles and descriptions, changesets, code comments, or docs.
- Comment only when the code genuinely needs explaining: a non-obvious reason, a gotcha, or an invariant. Never narrate what the code plainly does.
- Every
TODO/FIXMEmust reference a ticket, e.g.// TODO(#123): .... Do not leave a bare TODO. If no ticket exists, create one first.
- Conventional Commits (
feat:,fix:,chore:,docs:,ci:,test:). - Descriptive branch names (
feat/...,fix/...); never aclaude/or other tool-generated prefix.
- No em dashes in commit messages, code comments, PR or issue text, changesets, or docs. Use a comma, parentheses, or a separate sentence.
- All code quality checks must pass before you open a PR or call the work done. Run them and report the real output; do not open a PR while any check is failing.
- Commands:
npm run build(runstsc, which type-checks) andnpm test(vitest run);npm run coverageenforces the coverage thresholds. There is no separate lint/format tooling configured yet. Never claim a change works without running these. - Match the surrounding code's style, naming, and comment density.
- Install dependencies:
npm install - Build (type-check and emit):
npm run build(tsc, output indist/) - Run from source:
npm run dev -- <command>(tsx); or after building,node dist/index.js <command> - Commands:
init [name],templates,check,verify [flags],apps, and the instance-management commandsprofile,login,whoami,logout,sessions,config,users,org(all dispatched fromsrc/index.ts)
The entry point is src/index.ts, which dispatches to a command module in
src/commands/.
- init (src/commands/init.ts) scaffolds a project, driven by
src/prompts/. The web and api starters come from the registry-driven template source (src/core/templates.ts): it readsregistry.jsonfrom thefells-code/seamless-templatesmonorepo (pinned bySEAMLESS_TEMPLATES_REFin src/core/images.ts), downloads the selected templates, and applies each template'stemplate.jsonenv contract. The auth, docker, and config pieces are still generated locally insrc/generators/*. Override the template source for development withSEAMLESS_TEMPLATES_DIR(a local checkout) orSEAMLESS_TEMPLATES_REF(a different ref).- A
--<id>or--<alias>flag (e.g.seamless init --react-oauth,seamless init --oauth) preselects the matching template and skips that layer's prompt. Both spellings live in the registry, so no per-flag code.resolveTemplateAliasesruns inrunCLIbefore the project directory is created and before the non-empty-directory confirmation, so an unknown flag can never route through a destructive prompt on its way to an error. --yesruns the whole thing without prompting: every question has a flag (--web,--api,--email,--auth,--admin) and anything unspecified falls back to the option the prompt marks "(recommended)".--yesis never enough for a destructive step: overwriting a non-empty directory and rotating an existing service token both require--force, and choosing between a managed application and a local stack requires--appor--local. Flag parsing lives inparseInitArgs(src/index.ts); everything it produces is validated inrunCLIbefore a directory is created.- Every prompt is fronted by
requireInteractive(src/core/tty.ts), so a run without a TTY on stdin fails naming the flag that answers the question instead of rendering a prompt nobody can answer. This holds across every command, not justinit. When adding a prompt anywhere, guard it the same way. - templates (src/commands/templates.ts) lists the registry
(
seamless templates list [--json]) so those ids and flags are discoverable without a checkout. It reads the same sourceinitdoes and needs no login. - A template can declare
setup.oauthin itstemplate.jsonto trigger the OAuth provider prompts (src/prompts/oauthSetup.ts, catalog in src/core/oauthProviders.ts). The chosen providers are wired into the auth server env (OAUTH_PROVIDERS, per-provider*_CLIENT_SECRET, theoauthlogin method) bybuildAuthEnvin src/generators/docker/docker.ts.
- A
- destructive confirmations go through
confirmDestructive(src/core/confirmAction.ts), which answers itself when--forceis set and otherwise asks.--forceis the standing spelling for "do it without asking";hasForceFlagalso accepts--yesand-y, becauseconfig oauth-providers remove --yesshipped before the convention existed.--yesmeans something narrower oninit(answer the ordinary questions, never the destructive ones), so do not add--yesalone to a destructive step. Cancelling a confirmation reads as declining, not as an error. - check health-checks a running stack (local or managed).
- verify (src/commands/verify.ts) runs the conformance harness (below).
- instance management —
profile(targets, plusprofile login),logout/whoami,sessions,config(system config + OAuth providers),users, andorgall talk to a running instance and are authenticated by the stored session. - help —
seamless --help,seamless <command> -h/--help, andseamless help <command>all render from the single registry in src/commands/helpTopics.ts (src/commands/help.ts does the formatting, andCOMMANDSthere is also the dispatcher's known-command list). Document a new command or flag in that registry, not in the help template.src/index.tsanswers the help flag before a command parses its own args. - portal —
loginsigns in to the Seamless portal, a separate account from any instance profile. Its session lives beside the profile map inconfig.jsonand is the only oneinituses to connect a managed application (src/core/authClient.ts exposescreatePortalClientfor it).
seamless verify stands up the ecosystem with Docker Compose and runs a Playwright matrix, then
prints a flow x layer pass/fail grid (plus JUnit and HTML reports).
- verify/docker-compose.verify.yml: postgres, the auth API, and
both adapters, plus the React starter behind the
reactcompose profile. The mock OIDC provider runs in-process inglobal-setup(it is not a container). - verify/adapter-app (port 3000) and
verify/adapter-fastify-app (port 3001): minimal adopter backends on
@seamless-auth/expressand@seamless-auth/fastify, each with a capture transport so the harness can read OTP / magic-link codes the adapter would otherwise strip. They are deliberately twins: the same routes on the same env contract, so a spec cannot tell which one answered and any difference in behaviour is a real one. Keep them in step when either changes. - verify/harness: the Playwright projects (
api,adapter,adapter-fastify,react),lib/helpers,mock-oidc.ts,global-setup.ts, andlib/matrixReporter.ts(the printed grid). It has its ownnode_modulesand browsers.- The two adapter projects run the same specs from
./adapter; only theadapterUrlproject option differs (lib/fixtures.ts). Adding an adopter framework is a project entry plus a compose service, never a copy of the suite. Because they share a directory,matrixReportertakes the layer from the Playwright project name, not the spec's path.
- The two adapter projects run the same specs from
Modes and sibling repos:
--localbuilds the@seamless-auth/*packages from source (pre-publish contract testing); the default uses the published packages.- The browser layer runs once per web template, not once.
verifyreads the templates registry and drives everykind: webentry that is notcoming-soon, each served at :5173 in turn and scoped to the flow tags itstemplate.jsondeclares inverify.flows(the whole suite when it declares none). Soreact-oauthruns only@oauth, andreact-viteruns everything. - The sibling repos are resolved relative to this repo, overridable with
SEAMLESS_API_DIR,SEAMLESS_SERVER_DIR,SEAMLESS_REACT_SDK_DIR(the React SDK), andSEAMLESS_TEMPLATES_DIR(the templates checkout the web templates come from, defaulting to../seamless-templates).SEAMLESS_REACT_DIRis the narrower override: it names a single template directory and runs that one instead of the registry's set. - Useful flags:
--api-only,--no-react,--filter=<flow>(the=form; a space-separated--filter <flow>is not parsed),--keep-up.
- src/commands: one file per CLI command
- src/generators: locally generated scaffolding (auth, docker, config)
- src/core: shared helpers (templates, exec, env, fetch, secrets, paths, package manager, output)
- src/prompts: interactive setup prompts (
@clack/prompts) - verify: the conformance harness (shipped with the package)
Templates are not in this repo — they live in the seamless-templates monorepo
(SEAMLESS_TEMPLATES_REPO) and are fetched at scaffold time.
- TypeScript, ESM (
"type": "module"). Local imports use.jsextensions (NodeNext resolution). - Commit, comment, TODO, and attribution rules live in Working Standards above.
- Releases use Changesets. A user-facing change needs a changeset (
npm run changeset). A push tomainopens a "version packages" PR that bumps the version and writesCHANGELOG.md; merging that PR publishes to npm. Do not hand-edit the version orCHANGELOG.md. - npm publish token. The release workflow publishes with the
NPM_TOKENrepo secret. It must be a classic Automation token (full publish rights, bypasses 2FA) owned by an account with publish access toseamless-cli; a granular token restricted to a package allowlist cannot create or publish it and the registry returns a confusingE404on thePUT. - Templates ref bump. Shipping a change that depends on a new templates release is a two-step,
cross-repo dance: release
seamless-templatesfirst, then bumpSEAMLESS_TEMPLATES_REF(src/core/images.ts) to that tag. - Coverage badge.
README.mdshows a line-coverage badge (resources/coverage-badge.svg) regenerated locally by a Huskypre-commithook (.husky/pre-commit): it runsnpm run coverage(src/**/*.test.tsonly, so it never sweeps the Playwright specs underverify/), thennpm run coverage:badge(scripts/updateCoverageBadge.mjs) to rewrite the SVG fromcoverage/coverage-summary.json, stages it, and rebuilds. We standardized on the pre-commit hook (matchingseamless-auth-api) rather than a CI staleness check, so the committed badge always reflects the latest local run. If you change coverage, let the hook regenerate the badge; do not hand-edit the SVG.
.github/workflows/scaffold-smoke.yml runs
scripts/scaffold-smoke.sh on every PR: it scaffolds with
init --local --yes --auth=docker --admin=none, brings up db and auth from the
generated compose, then writes a row, recreates the container, and reads it back.
It exists because every other job asserts the generated files as strings. This is the only one that hands them to Docker, so it is the only one that can catch a compose file that is well-formed and wrong. Bring-up alone is not enough: a volume mounted where the image does not store data leaves a database that starts, passes its healthcheck, serves queries, and quietly writes to the container layer. The read-back is what catches that.
If you change src/generators/docker/docker.ts, expect
this job to be the one that fails. Run it locally with ./scripts/scaffold-smoke.sh; set
SMOKE_EXTRA_COMPOSE to an override file if you already have something on 5432 or 5312,
since the generated compose pins both the ports and the container names.
- Run
npm run build(the root package's only build step). - If you touched the harness:
cd verify/harness && npx tsc --noEmit, then runseamless verify(--localto exercise local SDK source, or--api-onlyfor a fast pass). - Add a changeset for any user-facing change.
- Sibling-repo branches: every sibling repo (api, server, react SDK, seamless-templates) is checked out
at its default branch (
main) when no explicit*-refis passed to the verify CI workflow. --localneeds SDK dependencies: it builds the server (pnpm) and the React SDK (npm) from source on the host, so those repos must have their dependencies installed first. CI installs them explicitly.- OAuth mock networking: the in-process mock OIDC is reached by the browser and harness via
localhost, but by the API container viahost.docker.internal, so the provider config splits the authorize URL from the token / userinfo URLs. - Adapter OTP limiter: the adapter funnels all OTP through one client IP, so the API's per-IP OTP limiter (10 per 15 minutes, hardcoded) bounds adapter / react OTP traffic. Keep specs off it where possible (for example, magic-link login instead of a second email-OTP round trip).
- Version pins: verify/adapter-app pins
@seamless-auth/express, verify/adapter-fastify-app pins@seamless-auth/fastify, and each web template pins@seamless-auth/react. Bump these when new versions publish. - The four pins in src/core/images.ts are what a scaffold gets, and each
drifts on its own:
SEAMLESS_AUTH_API_VERSION(the auth server image), the admin dashboard image and ref, andSEAMLESS_TEMPLATES_REF. Check them against the sibling repos' latest tags before a release; nothing fails when they lag, the scaffold just quietly ships an older stack. --auth=localis not pinned: itgit clonesseamless-auth-apiat its default branch (src/generators/auth/auth.ts) while--auth=dockerruns the pinned image, so the two auth modes can scaffold different servers from the same CLI version.- Config keys ahead of the API:
WRITABLE_KEYSin src/core/systemConfig.ts mirrors the instance's strict patch schema.magic_link_redirect_urisis currently ahead of it (defined in@seamless-auth/types, not yet released there, and not yet read by the auth API), so an instance rejects that key today. Adding a key here before the API accepts it makesconfig setfail against every live instance.