The review queue
A free submission waits for a person. The queue tells the submitter how long that is likely to
take, offers the paid plans as a way past it, and mails you once a day while anything is
waiting. It is part of features.submissions; the queue block in site.config.ts tunes it.
-
Set the two fields. Both have defaults, so the block may stay omitted.
site.config.ts queue: { estimateDays: 21, digestHourUtc: 9 },estimateDaysis what a site with no approval history tells submitters.digestHourUtcis the UTC hour of your reminder, ornullto switch it off.Terminal pnpm check:configsite.config.ts OK — <your site name> (https://your-domain.com). -
Choose where the digest goes.
site.config.ts admin: { email: 'you@example.com' },Without
admin.emailit goes tosite.contactEmail.providers.adminNotifypicks the channel — the same one the per-submission notification uses — and'off'silences both. -
Deploy.
Terminal pnpm deployThe hourly trigger
0 * * * *ships inwrangler.jsonc; keep it, because a deploy without it clears the schedule.
Verify
Section titled “Verify”Submit a free listing locally (pnpm dev, then /submit/) and read what it is told.
| What to check | Where | You should see |
|---|---|---|
| The estimate | the status page after “Submit for review” | “Expected to go live in about 3 weeks — number 1 in the queue.” on a fresh local database |
| The receipt | the submitted row in email_outbox |
a queue.wait of the same phrase, and queue.upgrades when paid plans are on |
| The tiles | /admin/ |
“In the free queue” counts it; “Wait submitters are told” shows the phrase with its basis underneath |
| The digest | fire the hourly job at the digest hour (below) | adminDigest=1 in the log line and an admin-digest row in email_outbox |
How it works
Section titled “How it works”The estimate is throughput, not a promise. The free queue is every row that is pending,
unpaid-by-design (payment_status n/a) and on the free tier — the first tier whose price is
zero. A submission’s position is the number of free submissions created before it, plus one. The
rate is the approvals of the last 30 days divided by 30, counting only rows with
source = 'submit': seeded, imported and admin-created listings never count as evidence that the
queue moves. Days to expect is the position divided by the rate, rounded up, at least one. With
no approvals in the window there is no rate, and queue.estimateDays is used instead.
The number becomes a phrase: up to a day, “within a day or two”; under a week, “in about N days”; under eight weeks, “in about N weeks”; beyond that, “in about N months”. It appears in three places. The status page says “Expected to go live … — number N in the queue.” while the submission is pending and free. The receipt email says “Expected to go live …” for the free tier. The dashboard tile “Wait submitters are told” shows it with its basis: “from N approvals in 30 days”, or “queue.estimateDays, until approvals set a rate”.
The upgrade. While a free submission is pending, its status page lists the enabled priced
tiers under “Skip the queue”, each with its price and review promise and an “Upgrade to …”
button that opens Stripe Checkout with ?tier= on the checkout link. The listing changes tier
only when the payment lands, so an abandoned checkout changes nothing. The receipt carries the
same offer as one sentence — “Skip the queue: Express is reviewed within 48 hours, … — upgrade
from your status page.” Both are absent when features.payments is off, the payment provider
is none, or no enabled tier has a price.
The daily digest runs inside the hourly job. At the tick whose UTC hour equals
queue.digestHourUtc it queues one admin-digest mail — unless one was queued in the last
twenty hours (a tick that runs twice sends once), the queue holds nothing approvable (pending and
free, or pending and paid), or providers.adminNotify is 'off'. It says how many are waiting
and how long the oldest has waited, what free-tier submitters are currently told and from how
many approvals, and which paid submissions are within a day of the review promise in their
tier’s slaHours. The button goes to /admin/. It leaves through the outbox on the
admin-notify channel, flushed by the same tick.
Firing the hourly job locally
Section titled “Firing the hourly job locally”Nothing runs the cron on a clock in local development; wrangler says so when it starts. The
built Worker, run under wrangler dev, answers the trigger endpoint by hand:
pnpm buildpnpm exec wrangler devcurl "http://localhost:8787/cdn-cgi/local/scheduled?cron=0+*+*+*+*"The terminal running wrangler dev logs one line:
jobs: expireFeatured=0 flushOutbox=0 retryRebuilds=0 purgeUnpaid=0 adminDigest=0 (cron=0 * * * *, in <n>ms)Add &time=<milliseconds since the epoch> to run the tick as if at that moment — pick one
inside the digest hour, with a free submission pending, and the line ends with adminDigest=1.
When it is off or degraded
Section titled “When it is off or degraded”| Configuration | What a submitter sees | What /admin/ shows |
Fix |
|---|---|---|---|
queue.digestHourUtc: null |
the estimate as before | the tiles as before; no digest arrives | set an hour |
providers.adminNotify: 'off' |
the estimate as before | no digest and no per-submission notification; nothing reported | set it to 'same' |
| no approvals in the last 30 days | “in about 3 weeks” on the defaults | the tile reads “queue.estimateDays, until approvals set a rate” | approve real submissions, or lower estimateDays |
providers.email: 'console' |
— | the digest is logged, not sent; warning: providers.email is "console": submitters get no mail, it is only logged. |
|
features.payments: false, or no priced tier enabled |
no “Skip the queue” section; no upgrade sentence in the receipt | — | Plans and payments |
the cron trigger removed from wrangler.jsonc |
the estimate as before | no digest; queued mail waits until something else flushes it | restore triggers.crons and redeploy |
Configuration
Section titled “Configuration”| Field | Default | What it changes |
|---|---|---|
queue.estimateDays |
21 |
the wait stated until real approvals set a rate; 1 to 365 |
queue.digestHourUtc |
9 |
the UTC hour of the digest, 0 to 23, or null for none |
admin.email |
unset | the digest’s recipient; falls back to site.contactEmail |
providers.adminNotify |
'same' |
the channel the digest travels on; 'off' disables it |
tiers[].slaHours |
unset | the review promise quoted in the upgrade offer and watched by the digest |
Reference: queue, admin, providers, tiers.
- site.config.ts the
queueblock,admin.email,providers.adminNotify - wrangler.jsonc
triggers.crons Directorysrc/
- worker.ts
scheduled(), the cron entry Directoryserver/
- queue.ts the estimate, the wording, the upgrade sentence, the one-query snapshot
- jobs.ts
runJobs()andsendAdminDigest() - queries.ts the dashboard’s queue numbers
Directoryemail/templates/
- admin-digest.ts the reminder
- submitted.ts the receipt with the wait
Directorypages/submit/status/
- [token].astro computes the estimate and the upgrade links
- templates/submit/StatusPage.astro “Skip the queue”
- templates/admin/DashboardPage.astro the tiles
- worker.ts
- tests/workers/jobs.test.ts the digest, the repeat guard, the snapshot