Skip to content

Access-cutoff reconciliation depends on a cron that may never run on Dokploy (#494 follow-up) #513

Description

@guillermoscript

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

  1. 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.
  2. 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.
  3. Fix docs/OPERATIONS_GUIDE.md:218-223, which still tells the operator to configure Vercel Cron.

Metadata

Metadata

Labels

Sub-taskbugSomething isn't workingpaymentsPayment provider / billing relatedseverity:highHigh severity security finding

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions