Skip to content

feat(studio): meter prompts on the server and count limits in postgres - #27

Merged
jm-rivera merged 4 commits into
devfrom
feat/studio-prompt-limits
Oct 20, 2026
Merged

jm-rivera merged 4 commits into
devfrom
feat/studio-prompt-limits

Conversation

@jm-rivera

@jm-rivera jm-rivera commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Studio forwarded every model turn while the guest limit lived in localStorage. Each POST /api/studio/turn atomically reserves one prompt from the caller's allowance in Postgres before forwarding. Guests get 5 prompts a day per network address, with IPv6 addresses grouped by /64, and signed-in users get 75 a day by user id. Both reset at 00:00 UTC. Guests behind a shared address share its allowance. Exhausted allowances return 429 with Retry-After. Database failures or the five-second admission deadline return 503. Refused requests consume no allowance. A failed turn refunds the exact window it reserved, so an old-day failure leaves the new day's usage intact.

The proxy forwards only turn, interrupt, cancel, and composition, matched as whole route names, so encoded paths cannot reach the harness's turn route without metering. refresh stays server-side with its own principal.

The per-IP limits on /api and /mcp move from Redis to the limit_counter table in migration 0017. All counter work uses a separate two-connection pool with a 2-second connection timeout, a 500ms client query timeout, and a 400ms Postgres statement timeout, including prompt reservations, refunds, and pruning. Per-IP limiter failures let requests through. Studio allowance failures refuse the turn. Counter stalls leave account and history connections available. ioredis and REDIS_URL are removed, and runtime pools log idle connection errors.

The generic countHit and refundHit APIs used by the stacked email-code sign-in PR #28 retain their signatures and behaviour.

Testing: pnpm typecheck, pnpm lint, pnpm build, and pnpm test pass, with 1,659 tests on Postgres 18.4. Regression tests cover concurrent reservations at the allowance, refused requests followed by refunds, refunds across midnight, Postgres row-lock cancellation, and pool exhaustion.

Every POST /api/studio/turn now spends a prompt before the proxy
forwards it. Guests get 5 a day per network address, an IPv6 address
counted by its /64, so a cleared or private browser starts no fresh
allowance. Signed-in users get 75 a day by user id. Both reset at
00:00 UTC, and a turn the harness does not start gets its prompt back
(src/lib/studio/spendPrompt.ts). The proxy forwards only turn,
interrupt, cancel and composition by exact name, so no encoded path
reaches the turn route unmetered.

The per-IP limiter for /api and /mcp moves from Redis, which
production never had, to the limit_counter table (migration 0017). It
counts on a two-connection pool that fails open within 2 s. ioredis
and REDIS_URL are removed.
pg-pool emits 'error' on the pool when an idle client's backend goes
away, as on a Cloud SQL restart or failover. With no listener Node
treats it as an uncaught error and the instance exits.
buildRuntimePool now attaches a listener that logs, so both pools in
src/lib/db/index.ts carry one. db/test/pool-idle-error.test.ts
terminates an idle backend and checks the pool keeps serving.
@jm-rivera
jm-rivera added this pull request to stack #39 October 20, 2026 13:42
@jm-rivera
jm-rivera marked this pull request as ready for review October 20, 2026 15:23
Reserve allowance only for admitted turns and refund the original UTC
window. Run reservations, refunds, and pruning on the bounded counter
pool so stalled counters leave account and history connections free.

Add regression coverage for concurrent reservations, refused attempts,
window rollover, and database connection and statement timeouts.
@jm-rivera
jm-rivera merged commit 9046d10 into dev Oct 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant