Follow-up to #493 / #494 — found while verifying the epic's delivered state (2026-07-24).
Problem
reconcileAccessCutoff() is the only thing that ever sets tenants.access_cutoff_at. For a tenant that simply grows past its plan limits (no plan-change event), the daily cron is the sole trigger — and that cron is declared only in vercel.json, while production runs on Dokploy.
Evidence
Call sites of reconcileAccessCutoff:
lib/billing/downgrade-tenant.ts (plan-change event)
lib/payments/platform-plan-change.ts (plan-change event)
app/actions/admin/billing.ts (plan-change event)
app/api/cron/enforce-plan-limits/route.ts (the only organic-growth path)
Notably absent: app/actions/join-school.ts (new student joins) and app/actions/teacher/courses.ts (new course created) — the two places that already run soft plan-limit checks and where usage actually crosses a limit.
Scheduling: /api/cron/enforce-plan-limits is registered in vercel.json at 0 3 * * *, alongside six other cron routes. docs/DEPLOYMENT.md is the Dokploy guide and documents CRON_SECRET but no scheduler; docs/OPERATIONS_GUIDE.md:218 still describes "Configure Vercel Cron". No GitHub Actions workflow hits /api/cron/*. The pg_cron job for platform expiry was deliberately unscheduled in supabase/migrations/20260719130000_platform_expiry_http_cron.sql in favour of the HTTP route.
Impact
If nothing is calling these routes on the Dokploy host, then: no cutoff is ever scheduled for organically over-limit tenants (#494 never fires for them), and platform subscriptions are never expired (#462's route never runs) — the whole non-payment lifecycle is inert in production. This is not specific to #494; it affects all seven cron routes.
Wanted
- Confirm whether a Dokploy scheduled task / host crontab is actually hitting
/api/cron/* with CRON_SECRET. If not, add one and document it in docs/DEPLOYMENT.md next to the env vars.
- Call
reconcileAccessCutoff() from join-school.ts and teacher/courses.ts so a limit crossing is caught at the moment it happens, with the cron as a safety net rather than the only mechanism.
- Fix
docs/OPERATIONS_GUIDE.md:218-223, which still tells the operator to configure Vercel Cron.
Follow-up to #493 / #494 — found while verifying the epic's delivered state (2026-07-24).
Problem
reconcileAccessCutoff()is the only thing that ever setstenants.access_cutoff_at. For a tenant that simply grows past its plan limits (no plan-change event), the daily cron is the sole trigger — and that cron is declared only invercel.json, while production runs on Dokploy.Evidence
Call sites of
reconcileAccessCutoff:lib/billing/downgrade-tenant.ts(plan-change event)lib/payments/platform-plan-change.ts(plan-change event)app/actions/admin/billing.ts(plan-change event)app/api/cron/enforce-plan-limits/route.ts(the only organic-growth path)Notably absent:
app/actions/join-school.ts(new student joins) andapp/actions/teacher/courses.ts(new course created) — the two places that already run soft plan-limit checks and where usage actually crosses a limit.Scheduling:
/api/cron/enforce-plan-limitsis registered invercel.jsonat0 3 * * *, alongside six other cron routes.docs/DEPLOYMENT.mdis the Dokploy guide and documentsCRON_SECRETbut no scheduler;docs/OPERATIONS_GUIDE.md:218still describes "Configure Vercel Cron". No GitHub Actions workflow hits/api/cron/*. The pg_cron job for platform expiry was deliberately unscheduled insupabase/migrations/20260719130000_platform_expiry_http_cron.sqlin favour of the HTTP route.Impact
If nothing is calling these routes on the Dokploy host, then: no cutoff is ever scheduled for organically over-limit tenants (#494 never fires for them), and platform subscriptions are never expired (#462's route never runs) — the whole non-payment lifecycle is inert in production. This is not specific to #494; it affects all seven cron routes.
Wanted
/api/cron/*withCRON_SECRET. If not, add one and document it indocs/DEPLOYMENT.mdnext to the env vars.reconcileAccessCutoff()fromjoin-school.tsandteacher/courses.tsso a limit crossing is caught at the moment it happens, with the cron as a safety net rather than the only mechanism.docs/OPERATIONS_GUIDE.md:218-223, which still tells the operator to configure Vercel Cron.