Skip to content

Faucet Service

The faucet is the platform's sign-up desk. A prospective user hands it an email; it checks the email, creates their account (via the platform service), and — for the free tier — seeds them at the fair-share cap so continuous accrual takes over from there. It emails the user their access token. Small FastAPI app on port 8500, backed by Postgres (creates its own database on first start).

The name is historical — it began as a credit "faucet" that dripped periodic top-ups. Today it's really the signup service; the ongoing top-ups now come from the credit service's fair-share accrual.

It's standalone — it owns its own tables and talks to just one service over HTTP: the platform service (/v1/auth/onboard), which does the actual granting. There's no weekly refill and the faucet no longer talks to the credit service directly — the credit service's fair-share accrual owns ongoing free credits.

Who gets what: free vs paid

Free tier Paid tier
Who University / allowed-domain email (.edu, …) Anyone else
Allowed to sign up? Always Only when open_registration is on
Starting credits Seeded at the fair-share cap + marked accrual_eligible None (buy paid credits via Stripe)
Ongoing credits Continuous fair-share accrual (credit service)

The faucet's real job is the free tier: verify the student, seed them, and hand accrual the keys. Paid users just get an account.

Signing up (the claim flow)

POST /v1/faucet/claim {email}:

  1. Check the email's domain → decide free or paid (reject a non-university email if open registration is off).
  2. Lock the email's claim row and check the cooldown — serializes two simultaneous claims for the same email, so a race can't create two accounts or seed twice.
  3. Call platform POST /v1/auth/onboard with seed_accrual = (tier == free). For a free user that seeds them at the fair-share cap and marks them accrual_eligible; a paid user just gets an account. If it fails, the whole thing rolls back — a failed sign-up doesn't burn the cooldown.
  4. Record the user, then email them their access token over SMTP (Brevo). If only the email fails, the account still exists and the response says so.

The token is the user's identity — there are no passwords. Ongoing credits come from the credit service's fair-share accrual (documented there), not the faucet.

Admin

All admin endpoints require Authorization: Bearer <ADMIN_TOKEN> and fail closed if the token is unset.

  • Registration toggle (/v1/faucet/pool) — open_registration gates whether non-university emails can sign up.
  • User management (/v1/faucet/users/…) — list users, read one user's status, change their tier, and revoke / restore them. Platform auth reads …/users/{email}/status on every request, so revoking a user here blocks their access everywhere.

Tables

Table Purpose
claims One row per email — last_claim timestamp for the cooldown / claim lock
users Registered users — user_type (free/paid), status (active/revoked)
pool_config Singleton — just open_registration

Endpoints

Method Path Auth What it does
GET /healthz Health check
POST /v1/faucet/claim Sign up with an email; emails back an access token
GET·PUT /v1/faucet/pool admin Read / toggle open_registration
GET /v1/faucet/users admin List registered users
GET /v1/faucet/users/{email}/status admin One user's status/tier (platform reads this for revocation)
PUT /v1/faucet/users/{email} admin Change a user's tier
POST /v1/faucet/users/{email}/revoke · /restore admin Disable / re-enable an account

Configuration

Variable Default Description
FAUCET_DATABASE_URL — (required) Postgres connection string (DB auto-created if missing)
FAUCET_PORT 8500 Service port
ADMIN_TOKEN Shared secret for admin + the onboard call
PLATFORM_URL http://platform:8100 Where accounts are created + seeded
ALLOWED_EMAIL_DOMAINS .edu,gmail.com,atomicmail.io Free-tier domains (comma-separated)
CLAIM_COOLDOWN_HOURS ~0.017 (≈1 min) Minimum gap between claims per email
SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/SMTP_FROM Brevo defaults Email delivery
CORS_ORIGINS * Allowed origins
RATE_LIMIT_PER_MINUTE 120 Per-IP rate limit

Running & testing

# Unit tests need a Postgres (test compose exposes one on host port 5433):
docker compose -f docker-compose.test.yml --profile faucet up postgres -d
pytest tests/faucet_service/ -v --ignore=tests/faucet_service/test_docker.py

# Full stack for the Docker integration test:
docker compose -f docker-compose.test.yml --profile faucet up --build -d
pytest tests/faucet_service/test_docker.py -v

Coverage: test_claim / test_claim_flows (domain rules, cooldown, tiers), test_claim_concurrency (no double-signup under a race), test_pool (open_registration), test_admin_users (user management), test_endpoints (claim + seed wiring), test_email_verify (SMTP), test_docker (deployed).