Code Review: Copy Shop Order System

Angular 22 SPA · Hono Cloudflare Worker · D1 + R2 · Reviewed Aug 28, 2026

Overall Assessment

The codebase is in good shape. Lint, unit tests, and worker tests all pass. The architecture is clean (shared API contracts, separated routes / queries / commands), SQL is parameterized everywhere, and idempotency is handled carefully. The main theme of this review: several things are intentionally "development-grade" and are currently enabled in the beta environment. They are fine for a beta behind Cloudflare Access, but each one is a blocker before a public or production launch.

Architecture
Strong
Security posture
OK for beta · gaps for prod
Data lifecycle
Needs work
Tests
Good · 8 worker + 7 unit + e2e
Lint / typecheck
Passing

What Is Working Well

Keep doing this

High Priority — Must Fix Before Production

High Identity simulator is enabled in beta and trivially forgeable

worker/identity.ts:19 · worker/routes/system.ts:33

assertLocalIdentityEnabled allows APP_ENV === "beta", and POST /api/local-session accepts any seeded user id with no credential — anyone can become any staff member by posting a user id, or by simply setting the copy_shop_local_user cookie themselves (it is not signed).

Action: acceptable while the beta is locked behind Cloudflare Access, but production must replace this with real authentication (e.g., Access service tokens / OIDC with signed sessions) and flip LOCAL_IDENTITY_ENABLED off. This is the single biggest pre-launch item.

High Customer endpoints are unauthenticated and unthrottled

worker/routes/customer-orders.ts:22, 38, 86

/api/orders/initiate, /api/uploads/:token, and /api/orders/:id/complete have no rate limiting. Anyone can create unlimited pending orders (each advertising up to 1 GB of files) and anyone who learns an order id or public id can call complete on it.

Action: before public exposure add rate limiting (Cloudflare WAF rules or a Workers rate-limit binding) and consider requiring the upload token / idempotency key on complete so the endpoint can't be called with a guessed public id.

High Access gate is duplicated and the beta bundle is fully inlined

worker/index.ts:11 · scripts/generate-beta-worker.mjs:99, 54

The ctx.access check exists twice: once in worker/index.ts and again in the generated beta worker. If one copy changes, the other silently drifts. Additionally, the generator embeds every built asset as base64 inside a single TypeScript file — roughly 33% size inflation with no compression — which will eventually hit Worker size limits as the app grows.

Action: prefer the standard Workers Static Assets setup (like wrangler.jsonc uses) for beta instead of the code-generated bundle, which removes both the duplication and the inlining. If the generated approach must stay, extract the Access check into one shared function.

Medium Priority — Fix Soon

Medium No cleanup of abandoned pending orders and orphaned uploads

worker/orders/commands.ts:41 · worker/routes/customer-orders.ts:44

Pending orders and their files rows expire after one hour, but nothing ever deletes them. Customers who abandon checkout will accumulate dead rows in D1 and unreferenced objects in R2 forever.

Action: add a scheduled handler (Cron Trigger) that deletes orders still pending past their upload expiry, cascading their files rows and R2 objects.

Medium Staff order list has no pagination

worker/orders/queries.ts:98-125

listOrders loads every matching order and returns total: rows.length. Fine at seed volume, but a few thousand orders will slow the workspace and bloat every response. The counts query also ignores the active filters, so tab badges may not match the filtered list the user sees.

Action: add LIMIT/OFFSET (or keyset) pagination and decide whether counts should respect the department/query filters.

Medium Shop timezone is hardcoded and duplicated

worker/order-validation.ts:94 · worker/orders/commands.ts:223 · src/app/core/format.ts

"America/Chicago" appears in at least five places across worker and client, and shopToday() is implemented twice (worker and UI). If the shop changes location or DST rules handling needs adjustment, there are many places to update.

Action: move the timezone constant and shopToday() into shared/order-contracts.ts (or a new shared/ module) and import from both sides.

Medium Upload progress can update the wrong file

src/app/customer/customer-order.component.ts:92

Progress callbacks match files by file.name. If a customer attaches two files with the same name, both progress bars jump together and either could be marked complete early. (The upload itself is correctly matched by index in order-api.service.ts:36 — only the progress display is affected.)

Action: key progress by upload.fileId from the initiate response instead of by name.

Low Priority — Nice to Have

Low Assorted small items

Suggested Order of Work

StepItemWhy this order
1Pending-order cleanup cronSmall, self-contained, prevents data growth in beta right now.
2Pagination + count consistencyProtects the staff workspace as real orders accumulate.
3Rate limiting on public endpointsNeeded before the beta link leaves your control.
4Replace identity simulatorBiggest change; do it once the data model is stable.
5Drop generated beta bundle for standard assetsRemoves the drift risk and size ceiling; can pair with step 4.
6Timezone consolidation + low itemsRefactor polish, any time.