Skip to main content

tai42-accounts-postgres

License: Apache 2.0

The Postgres-backed accounts provider for the TAI ecosystem — an installable plugin that owns human user accounts, password login, sessions, and invites. It registers itself as the "accounts-postgres" provider and mints/validates its own tai-sess-… session tokens.

Importing the package registers the provider in tai42-kit's module-level accounts registry (tai42_kit.accounts.registry, register_accounts_provider("accounts-postgres", ...)), which ALSO lands the factory in the identity registry under the same name — an accounts provider is the token answerer for its own sessions, so one registration keeps sessions both mintable and validatable. No tai42_app handle is involved, so it registers in any process that imports it. A deployment selects it by including accounts-postgres in the access-control auth_providers list.

Its only tai-* dependencies are tai42-contract (the accounts ABC, the injected admin-services and settings Protocols, and the login-method metadata models) and tai42-kit (the registry it registers through, the Postgres and Redis clients and the session/invite hash). It never imports the skeleton — the plugin is contract-facing, and the import is banned by ruff.

The TAI ecosystem

TAI is an open-source runtime for MCP tools, agents, and workflows. An accounts provider owns human sign-in: it authenticates a person, mints the session token their browser carries, and answers that token back as an identity on every later call. This package is one such provider (Postgres-backed accounts with password login, sessions, and invites); any package can back the same contract, so this repo is this provider's own full doc home, and the documentation site covers the platform-level story:

What it stores

Three plugin-owned tables in the platform database, created by the plugin's own migration chain:

  • accounts_users — one row per human user: the opaque, stable user_id (usr-…), the normalized email, the argon2id password hash (NULL until an invite is accepted), and the disabled flag. A role is not stored here: the platform's Members listing joins a member's role from the principal it holds, and an invitation's role is the one its principal holds, read through AccountsAdminServices.principal_roles.
  • accounts_sessions — one row per live login: the SHA-256 hash of the tai-sess-… token (the raw token is never stored), with sliding-idle (last_seen_at) and absolute (absolute_expires_at) expiry.
  • accounts_invites — one pending invite per user: the SHA-256 hash of the tai-inv-… token, its TTL, and its single-use consumption marker.

Rate-limit counters live in Redis (reached through the injected settings.redis), namespaced per deployment.

The plugin NEVER touches the skeleton's access_control_policies / access_control_routes tables or any ac:* Redis key directly — all policy and role writes go through the injected AccountsAdminServices (apply_role / remove_policy / set_user_disabled), and role reads through its principal_roles. The last-admin rule is the platform's: those services refuse a change that would leave no enabled admin principal (any admin principal counts, not only accounts), and the member actions surface the refusal as a 409.

HTTP surface

Public (/api/login/*, always-public prefix):

Route Does
POST /api/login/password Verify email + password, mint a session. Failures-only throttling; uniform 401; argon2 verify on every attempt (503 load-shed under a hash flood).
POST /api/login/invite/accept Consume an invite, set the first password, mint a session.

Authed (/api/auth/users*, reserved prefix):

Route Does
PUT /api/auth/users/me/password Change your own password; every OTHER session is revoked.

Self-service password route stays. PUT /api/auth/users/me/password is self-service, not member administration: it acts on the caller's OWN credential, targets no member row, and is open to every signed-in user (carved out of the admin fence with self_service=True). It is therefore NOT part of the generic member-actions seam and is the one route this module still ships. Member administration — inviting a user, sending a new login link, cancelling an invitation, changing a member's role or access, and removing a member — is no longer bespoke routes or a bespoke Studio page here: the provider declares those actions (member_actions) and performs them (invoke_member_action) through the platform's generic member-actions seam, which the Studio's own generic Members page renders. The one-time invite link a new invite or a resend produces surfaces on that action's result.

Logout is NOT here — the skeleton owns the single POST /api/auth/logout dispatcher; this plugin contributes revoke_session.

Configuration

Behavior config is the plugin's own TAI_ACCOUNTS_* namespace; the Postgres connection resolves through the central database registry on the component's binding (the plugin never reads skeleton config):

Env var Default Meaning
TAI_DB_BINDING_TAI42_ACCOUNTS_POSTGRES default The named database in the central registry the plugin's tables live in. The bound database is declared under TAI_DATABASE_<NAME>_PG_*; the default binding is TAI_DATABASE_DEFAULT_PG_*.
TAI_ACCOUNTS_SESSION_IDLE_SECONDS 86400 Sliding-idle session expiry.
TAI_ACCOUNTS_SESSION_ABSOLUTE_SECONDS 2592000 Absolute session cap from mint.
TAI_ACCOUNTS_INVITE_TTL_SECONDS 259200 Invite validity from mint.
TAI_ACCOUNTS_LOGIN_BACKOFF_THRESHOLD 5 Consecutive per-account failures before backoff.
TAI_ACCOUNTS_LOGIN_BACKOFF_CAP_SECONDS 900 Max per-account backoff lock.
TAI_ACCOUNTS_LOGIN_IP_MAX_ATTEMPTS 30 Per-IP failed attempts per window.
TAI_ACCOUNTS_LOGIN_IP_WINDOW_SECONDS 900 Per-IP fixed window.
TAI_ACCOUNTS_LOGIN_HASH_CONCURRENCY 2 × CPU count Max concurrent argon2 verifies (load-shed above).
TAI_ACCOUNTS_LOGIN_HASH_WAIT_SECONDS 2.0 Wait before a login sheds with 503 under hash saturation.
TAI_ACCOUNTS_REDIS_KEY_PREFIX = pg_db Per-deployment Redis namespace (derived from pg_db when unset).

First owner. The first owner is created by the platform's one-step setup door, not by this plugin. That door creates the owner principal and, when this provider is configured, attaches the owner's login through it (a password set now, or an invite link the owner follows later). The provider implements the login-attachment seam and ships no first-owner route of its own.

Shared Redis / shared pg_db: two deployments that share one Redis AND one pg_db must set distinct TAI_ACCOUNTS_REDIS_KEY_PREFIX values, or they will cross-read each other's rate-limit counters.

Proxies: the per-IP throttle keys on the platform's client resolver: the direct peer unless the deployment declares its proxies (TAI_RATE_LIMIT_TRUSTED_PROXIES / TAI_RATE_LIMIT_TRUSTED_HOPS), in which case the forwarded client; an IPv6 client counts by its /64. Behind a proxy with no declared trust, all callers collapse to one throttled address.

Schema migrations + startup guard

The plugin ships an ordered SQL migration chain under migrations/ and declares it in tai-plugin.yml (migrations: migrations). The shared migration runner applies it — run by the operator with tai db migrate, or automatically by the marketplace install/upgrade flow once the package lands. Applied migrations are recorded in the per-database tai_schema_history table under the component tai42-accounts-postgres, on the DDL-privileged migrator identity.

tai db migrate     # apply every pending migration across all discovered components
tai db status      # per-component applied / pending / checksum verdicts

The provider's boot healthcheck asserts the chain is fully applied — a pending migration or a checksum mismatch fails startup loudly naming tai db migrate — and fires only when the accounts store is configured (the database bound by TAI_DB_BINDING_TAI42_ACCOUNTS_POSTGRES resolves a non-empty password). It NEVER auto-applies.

The accounts kind requires access control ENABLED. If the routes are mounted while ACCESS_CONTROL_ENABLE=false (so the provider is never instantiated and the admin services are never injected), boot fails loudly rather than serving a broken door.

Deployment wiring

In the deployment manifest:

lifecycle_modules: ["tai42_accounts_postgres"]                 # provider registration
routers_modules: ["tai42_accounts_postgres.routes_login",
                  "tai42_accounts_postgres.routes_users"]       # login + self-service password

Member administration is the Studio's own generic Members page over the provider's declared member actions, so the plugin ships no Studio bundle of its own.

and in access control (example alongside the api-key provider):

ACCESS_CONTROL_ENABLE=true
ACCESS_CONTROL_AUTH_PROVIDERS=["accounts-postgres","redis"]

Security model

  • argon2id password hashing (RFC 9106 library defaults).
  • Hashed at rest: session and invite tokens are stored only as their SHA-256 hash; the raw token appears exactly once, in the response that mints it.
  • Uniform login failure: unknown email and wrong password return a byte-identical generic 401, and BOTH run a real argon2 verify (a dummy-hash verify on unknown email) so timing does not enumerate users.
  • Failures-only rate limiting: a correct password is never blocked — only a failed attempt records against the per-account and per-IP counters (the per-IP dimension keys on the platform's client resolver under the declared proxy trust). The argon2 verify is additionally bounded by a concurrency semaphore that sheds with a 503 under a hash flood. Redis being down fails the throttle CLOSED.
  • No-email invites: the plugin returns an origin-relative login_path for the admin to hand over; it never sends email and never fabricates an absolute URL.
  • Owner login attached at setup: the platform setup door owns first-owner creation; this provider only attaches the owner's login (password or invite) to the already-created owner principal, never creating a second owner.

Invite and session tokens are shown once — the raw invite link appears only in the result of the invite / resend member action, and the raw session token only in the login response that mints it.

Requirements

Requires Python 3.13+, a Postgres reachable through the database bound by TAI_DB_BINDING_TAI42_ACCOUNTS_POSTGRES (default TAI_DATABASE_DEFAULT_PG_*), and a Redis reachable through the injected access-control Redis. Apply the schema with tai db migrate before first serve; an out-of-date schema is caught loudly at boot.

Install

Requires Python 3.13+. Install from PyPI into the environment that runs the server:

uv add tai42-accounts-postgres

Or from source — clone this repo and add it as an editable dependency; the tai42-* dependencies resolve in-tree from the workspace.

git clone https://github.com/tai42ai/tai42   # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai42/plugins/accounts-postgres

Development

Python (from the repo root):

uv venv --python 3.13
uv pip install --no-sources --editable ".[dev]"
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright
uv run --no-sync pytest --cov --cov-report=term-missing

Coverage is gated at 95% (fail_under in pyproject.toml). CI installs with uv sync --locked --python 3.13 --extra dev, so a stale uv.lock fails there.

License

Apache-2.0. See LICENSE and NOTICE.

Metadata

Release files for tai42-accounts-postgres 5.0.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tai42-accounts-postgres 5.0.7
File Size Uploaded
tai42_accounts_postgres-5.0.7.tar.gz 45.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tai42-accounts-postgres 5.0.7
File Interpreter ABI Platform
tai42_accounts_postgres-5.0.7-py3-none-any.whl Python 3 none any Details

Total release size: 92.7 kB

Release files / tai42_accounts_postgres-5.0.7.tar.gz

Download URL tai42_accounts_postgres-5.0.7.tar.gz
Size 45.2 kB
Tags Source
SHA-256 checksum
How to use checksums
34624b27bd0710e37f5a51ce1e8da18f3ec96280b2f5e0cf8e4c7f3dd9be2b59
BLAKE2b-256 checksum
How to use checksums
38f1af557ddfdb953b86dc1a54a4dbc11680b51030070167b8534b1f863c46cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / tai42_accounts_postgres-5.0.7-py3-none-any.whl

Download URL tai42_accounts_postgres-5.0.7-py3-none-any.whl
Size 47.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
020a2995e96cf409615d958ac7e7b72db4a1406945dbca3a62c2b3df1e7b6749
BLAKE2b-256 checksum
How to use checksums
049aa7cd56b25a8c339bb77995f69f5b7f3db9b89faddfc57965a5a8422253c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

5.0.7 This release

2 release files

5.0.6

2 release files

5.0.5

2 release files

5.0.4

2 release files

5.0.3

2 release files

5.0.2

2 release files

5.0.1

2 release files

5.0.0

2 release files

4.0.4

2 release files

4.0.3

2 release files

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.2.1

2 release files

3.2.0

2 release files

3.1.4

2 release files

3.1.3

2 release files

3.1.2

2 release files

3.1.1

2 release files

3.1.0

2 release files

3.0.6

2 release files

3.0.5

2 release files

3.0.4

2 release files

3.0.3

2 release files

3.0.2

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.4.9

2 release files

2.4.8

2 release files

2.4.7

2 release files

2.4.6

2 release files

2.4.5

2 release files

2.4.4

2 release files

2.4.3

2 release files

2.4.2

2 release files

2.4.1

2 release files

2.4.0

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.3

2 release files

2.2.2

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.4

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page