Production Readiness Report
You shipped it.
Is it safe to operate?
We audited laddr across 9 engineering areas - 77 findings, 33 of them critical or high. Here's the picture.
laddr @ 97d133c
9 areas evaluated
Each area scored 0-10 by an AnchorStack engineer.
Security
2/10
The app has a workable authentication base, and many API routes verify bearer tokens before querying. It is not currently safe for production: direct database access can affect paid entitlements, real-looking secrets were present, and payment entitlements are split across non-atomic paths.
Architecture
5/10
The base shape is appropriate for a small product, but several important workflows rely on unclear state ownership, request-time recomputation, and unbounded reads.
Data
4/10
The data layer has reasonable constraints and useful indexes, but it is not yet safe to evolve or recover. Migration replay, restore evidence, and atomic database contracts need attention.
AI Engineering
4/10
Agent-facing scaffolding is stronger than many small product repos, but context controls are not locked down against credential exposure or unsafe production changes.
Infrastructure
3/10
The hosting and service shape is plausible, but it cannot currently be rebuilt, changed, scaled, or recovered safely from repository evidence alone.
Observability
2/10
The app has product analytics and platform logs but lacks minimum viable production observability: error tracking, uptime monitoring, structured correlated logs, routed alerts, job heartbeats, SLOs, and an incident runbook.
Code Quality
4/10
The codebase builds and typechecks, but production-critical paths are concentrated in large files and are not protected by meaningful tests or CI quality gates.
Delivery
3/10
There is not yet a repeatable, gated, traceable production release process appropriate for an app with payments, privileged database access, and automation.
Documentation
3/10
Documentation is partial and sprawled, with no trustworthy canonical entry point for deployment, secret rotation, recovery, or production operation.
Where to look first
01
— Security
Direct Database Access Can Self-Grant Paid Entitlements
The app appears designed to route database access through privileged API handlers, but a redacted data policy allows authenticated users to affect paid-access records with an insufficient guard.
Any authenticated user can issue a direct database write that grants paid subscription access without completing a payment. This bypasses Stripe entirely and creates entitlements the billing system has no record of, making them invisible to audits and refund logic.
Move all paid entitlement writes behind a server-side function that verifies a completed Stripe payment before writing. Add an integration test that attempts a direct entitlement write without a payment record and asserts it is rejected. Rotate the database credentials used in affected routes.
02
— Security
Real-Looking Secrets Are Present Locally And .env Was Committed To Version History
A local environment file contained multiple high-value credential classes, and a previous history state included private configuration that should be treated as exposed until rotated.
The committed file contained API keys and service tokens that are likely still valid. Anyone with read access to the repository history has had access to production credentials. This represents a confirmed exposure window of indeterminate length.
Immediately rotate all credentials present in the commit history. Run a BFG Repo Cleaner pass to purge the file from history. Add a pre-commit hook and CI check using a secret scanner to prevent future leakage. Audit recent access logs for the exposed credentials.
03
— Security
Payment Recording Has Competing Writers And Incomplete Entitlement Data
Two payment confirmation paths can both create purchase records. Concurrent or replayed calls can produce duplicate state, and one path omits fields written by the other.
Duplicate purchase records will cause customer support issues, incorrect billing counts, and failed refund lookups. The missing entitlement fields on one path mean some users get partial access states that are hard to detect or correct without manual intervention.
Designate the Stripe webhook handler as the sole canonical writer for purchase records. Add an idempotency key on the purchase table keyed to the Stripe payment intent ID. Remove the secondary confirmation path or reduce it to a read-only check against the webhook-created record.
04
— Security
Profile Update Endpoint Allows Mass Assignment Of User Columns
An authenticated profile update path accepts a broad object and writes remaining fields to the user row after removing only a narrow set of disallowed keys.
Authenticated users can overwrite any user column not in the narrow denylist, including role flags, subscription tier, and internal metadata fields. The denylist approach guarantees future columns will be exposed by default when the schema evolves.
Replace the denylist with an explicit allowlist of updatable profile fields on the server side. Add tests that attempt to write restricted fields and assert they are silently ignored or rejected. Review the schema for any columns that may have already been written this way.
05
— Security
Completion APIs Trust Client-Supplied Scores And Fail Open
Completion endpoints persist client-supplied scoring and progress data without recomputing sensitive values at the server boundary.
Any user can submit arbitrary scores and progress values. This affects leaderboard integrity, word completion counts, and streak tracking — all of which the product uses for engagement and potentially for premium feature gating.
Recompute all trusted gameplay values server-side using the canonical game logic. Validate client-submitted values against the server computation before persisting. Add fuzz tests that submit out-of-range values and assert they are rejected or clamped.
06
— Security
Stripe Webhook Signature Verification Is Conditional
The Stripe webhook handler verifies the signature only when a specific environment variable is present, allowing unsigned events to be processed in environments where that variable is absent.
Any actor who can reach the webhook endpoint can forge a payment event and trigger the purchase confirmation flow without a real Stripe transaction. This directly enables unauthorized entitlement grants at scale.
Make signature verification unconditional. If the secret is absent, reject the request with a 400 and log the configuration error. Add an integration test that sends an unsigned webhook payload and asserts it is rejected.
07
— Security
No Rate Limiting On Authentication Or Password Reset Endpoints
Login and password reset routes have no IP-based or token-based rate limiting, leaving them open to credential stuffing and enumeration attacks.
An attacker can attempt unlimited credential combinations against user accounts. Password reset can be used to enumerate valid email addresses via timing or response differences. Both represent account takeover vectors for any user in the system.
Add rate limiting to auth and password reset routes using an in-memory or Redis-backed limiter. Return a consistent response time and body regardless of whether the email exists. Add monitoring alerts for high request rates to these endpoints.
08
— Security
Session Tokens Are Not Rotated After Privilege Changes
When a user upgrades to a paid subscription, the existing session token is not invalidated or rotated, leaving pre-upgrade sessions valid with post-upgrade privileges.
A compromised pre-upgrade session token continues to grant paid-tier access indefinitely. If a user's free session was stolen before they purchased, the attacker gains paid access without any re-authentication.
Rotate session tokens on any privilege change (payment, role change, password reset). Implement a session version counter on the user record and reject tokens that predate the current version.
09
— Architecture
Word Completion History Uses Unbounded Full-Table Reads
Leaderboard and completion count queries scan the entire completions table without user-scoped indexes or pagination, growing linearly with total app usage.
As the user base grows, leaderboard and completion queries will degrade unboundedly. On a table with 100k+ rows, these queries will time out under normal load, causing gameplay features to fail for all users simultaneously.
Add a composite index on (user_id, completed_at) and a separate index for leaderboard queries. Introduce pagination or cursor-based access for any endpoint that returns completion history. Add a slow-query threshold alert.
10
— Architecture
Streak And Score State Is Recomputed On Every Request
User streak and aggregate score values are recalculated from raw completion records on each API call rather than maintained as cached or materialized values.
Each request to display a user's dashboard triggers a full aggregation over their completion history. This creates N+1 query patterns at scale and makes the dashboard page progressively slower as users accumulate more completions.
Maintain streak and score as denormalized columns on the user record, updated atomically alongside each completion write. Use a database trigger or application-level transaction to keep them consistent. Cache at the API layer for read-heavy paths.
11
— Architecture
Subscription State Is Owned By Multiple Systems With No Canonical Source
Whether a user has an active paid subscription is determined by querying both the purchase table and the user record, with no single authoritative field.
Inconsistent subscription checks mean different parts of the app can disagree on whether a user is a paying customer. This creates support cases where users lose access intermittently, and makes it impossible to write reliable subscription audits.
Designate a single canonical boolean field (e.g., users.is_subscribed) as the source of truth for subscription state. All subscription checks in the application should read only this field. Updates to the field happen only from the Stripe webhook handler.
12
— Data
Database Migrations Cannot Be Replayed From A Clean State
The migration history contains mutations that assume prior state and will fail if run against a fresh database, making it impossible to provision a new environment or verify the schema from scratch.
You cannot provision a new developer environment, staging environment, or disaster-recovery database without manual intervention. Any new team member or automated test environment will fail to set up. This also means your schema drift risk is undetected.
Audit all migrations for idempotency — each migration should be runnable against a clean database. Add a CI step that provisions a fresh database and runs all migrations end-to-end. Fix any migration that assumes pre-existing data or schema state.
13
— Data
No Evidence Of Backup Verification Or Restore Testing
There is no documented backup schedule, no restore test record, and no runbook for recovering the database from a backup in the repository.
A database failure or accidental data loss has no verified recovery path. Backups that have never been tested are frequently found to be incomplete or corrupt at the moment they are needed. For an app with payment records and user data, unrecoverable data loss is a GDPR incident.
Document the backup provider and schedule. Add a monthly restore test to a staging environment and log the result. Create a recovery runbook that specifies RTO and RPO targets and the step-by-step restore procedure.
14
— Data
Multi-Step Payment Writes Are Not Wrapped In A Transaction
The payment confirmation flow writes to multiple tables sequentially without a wrapping database transaction, leaving the database in a partially-written state if any step fails.
A network error or exception mid-write can create a purchase record without a corresponding entitlement row, or vice versa. These half-written states are invisible to the app and require manual database inspection and correction to resolve.
Wrap all multi-table payment writes in an explicit database transaction. Ensure the transaction rolls back completely on any failure. Add an integration test that simulates a failure mid-write and asserts the database remains in a consistent state.
15
— Data
Schema Has No Soft-Delete Or Audit Trail For User Data
User and purchase records are hard-deleted with no soft-delete flag or audit log, making GDPR erasure requests and accidental deletion unrecoverable.
If a user is deleted accidentally or maliciously, their data and purchase history are unrecoverable. GDPR right-to-erasure requests cannot be logged as completed. Support cases involving deleted accounts have no recovery path.
Add a deleted_at timestamp to user and purchase tables. Implement soft-delete across the application. Maintain a separate audit log table for all record mutations. Ensure the GDPR erasure process nullifies PII fields rather than hard-deleting rows.
16
— AI Engineering
Agent Context Includes Production Database Credentials Scope
The MCP server configuration grants agent sessions access to tools that can read and write production database records without scope restriction.
An AI agent operating in development or with a compromised prompt can make writes to the production database. There is no sandbox or read-only mode for agent sessions, and no audit trail of what agents have changed.
Create a read-only database role for agent sessions. Restrict agent tool scope to non-destructive operations by default, requiring explicit escalation for writes. Add an audit log for all agent tool calls that includes the session context and tool arguments.
17
— AI Engineering
No Guardrails Against Prompt Injection Via User-Controlled Content
Agent workflows process user-submitted content (word inputs, notes) without sanitizing for prompt injection patterns before passing to the model context.
A user can craft a word submission or note that hijacks the agent's instruction context, potentially causing the agent to leak other users' data, bypass access controls, or take destructive actions within its tool scope.
Sanitize all user-controlled strings before they enter model context. Add a system prompt layer that states role boundaries and explicitly forbids instruction override. Implement output validation that checks agent responses for signs of injection before acting on them.
18
— AI Engineering
Agent Sessions Have No Token Budget Or Cost Controls
Agent tool calls have no per-session token budget, no cost ceiling, and no circuit breaker to halt runaway sessions before they accumulate significant API charges.
A malformed prompt, infinite loop in agent logic, or adversarial input can trigger an unbounded chain of model calls. This has directly caused multi-thousand-dollar API bills in similar systems. There is currently no automatic stopping mechanism.
Implement a per-session token budget enforced at the agent orchestration layer. Add a cost circuit breaker that halts execution and alerts when a session exceeds a configurable threshold. Log token usage per session to a monitoring dashboard.
19
— Infrastructure
Infrastructure Cannot Be Rebuilt From Repository Alone
There is no infrastructure-as-code, no documented provisioning steps, and no record of which cloud resources exist and how they are configured.
If the hosting account is lost, suspended, or needs to be migrated, there is no way to reconstruct the environment. Reproducing the production setup requires tribal knowledge and manual configuration, with high risk of misconfiguration and extended downtime.
Document all cloud resources and their configuration in a runbook or as IaC (Terraform, Pulumi, or similar). At minimum, create a provisioning checklist that covers database, hosting, DNS, secret stores, and third-party service configuration.
20
— Infrastructure
No Documented Scaling Approach For Traffic Or Database Load
There is no documented scaling plan, no defined traffic thresholds that would trigger scaling actions, and no evidence that the current infrastructure has been load-tested.
A traffic spike (product launch, press coverage, social mention) will hit an infrastructure that has never been validated under load. The most likely failure points are the database connection pool and the Stripe webhook processing queue — both of which fail without clear error messages.
Define scaling thresholds for compute and database. Run a load test against staging at 10x expected peak traffic. Document the scaling actions to take at each threshold. Configure auto-scaling or connection pooling before the first major traffic event.
21
— Infrastructure
No Recovery Runbook Or Documented Incident Response Process
There is no documented procedure for responding to a production incident — no runbook, no escalation path, and no defined RTO or RPO.
When an incident occurs, response time is determined by whoever is available and what they can remember. Without a runbook, mean time to recovery is significantly longer, and the same incidents recur because root causes are not systematically addressed.
Create a minimal incident runbook covering: how to identify the scope of an outage, how to roll back a bad deploy, how to restore the database from backup, and how to communicate status to users. Assign an on-call rotation even if it is a single person.
22
— Infrastructure
Third-Party Service Dependencies Are Not Health-Monitored
Stripe, Resend, and the database have no health checks, no circuit breakers, and no automatic alerting if they become unavailable.
When Stripe or the database goes down, the app fails silently from the user's perspective and noisily via unhandled exceptions in logs that no one is watching. There is no automatic notification to the operator and no graceful degradation path.
Add health check pings for each critical dependency with alerting on failure. Implement graceful degradation — for example, queuing failed email sends for retry rather than surfacing a 500. Set up a status page or at minimum a Slack alert channel for dependency incidents.
23
— Observability
No Error Tracking — Exceptions Are Silent In Production
Application exceptions are not captured by any error tracking service. Unhandled errors in API routes and server components are visible only in raw platform logs that are not monitored.
Production bugs, including payment failures and auth errors, can go undetected indefinitely. The first signal of an outage is typically a user complaint rather than an automated alert. Error rates, error types, and affected users are completely unknown.
Integrate an error tracking service (Sentry, Highlight, or equivalent). Instrument both server-side API routes and client-side rendering paths. Configure alert thresholds for error rate spikes. Ensure payment and auth errors are captured with full context.
24
— Observability
No Uptime Monitoring Or Automated Alerting
There is no external uptime monitor checking that the app is reachable and returning valid responses. Outages are detected only when a user reports them.
The app can be fully down for hours before anyone is aware. For a subscription product, undetected downtime directly causes churn. Payment webhooks that fail silently during an outage create billing state divergence that is difficult to reconcile.
Set up an external uptime monitor (Better Uptime, Cronitor, or equivalent) with HTTP checks on the homepage and a critical API endpoint. Configure PagerDuty or a Slack alert for any downtime exceeding 2 minutes. Add a synthetic payment flow check to catch partial outages.
25
— Observability
Scheduled Job Automation Has No Heartbeat Or Failure Alerting
The app has scheduled automation jobs (word refresh, streak calculations) with no heartbeat monitoring — if a job stops running, there is no alert.
A silently failing scheduled job degrades the product experience invisibly. Users who rely on daily word refreshes or streak tracking get stale data without any indication of a problem. The issue can persist for days or weeks before a user complaint surfaces it.
Integrate a heartbeat monitor (Cronitor, Healthchecks.io) for each scheduled job. Each job should ping the monitor on completion. Configure an alert for any job that misses its expected window. Add a last_run_at column to the job log for dashboard visibility.
26
— Observability
No Structured Logging — Correlation Across Requests Is Impossible
Log output is unstructured console output with no request ID, user ID, or trace context, making it impossible to reconstruct what happened during a specific user's session or payment flow.
When investigating a support case or incident, there is no way to filter logs to a specific user, request, or payment. Every investigation requires manually scanning raw log output. This makes debugging payment failures and auth issues substantially slower and more error-prone.
Add a request ID header (or generate one per request) and propagate it through all log statements. Use structured JSON logging with fields for request_id, user_id, route, and duration. Configure your log aggregation platform to index these fields for fast filtering.
27
— Code Quality
Payment And Auth Logic Is Concentrated In Large Untested Files
The files responsible for payment processing and authentication contain the majority of business logic but have no unit or integration test coverage.
Any change to payment or auth logic — including security patches — is made without a safety net. Regressions in these paths will be caught only by users in production. The high coupling makes it difficult to change one behavior without inadvertently affecting another.
Extract payment and auth logic into discrete, injectable service functions. Write integration tests for each happy path and critical error path (failed payment, expired session, invalid token). Add these test files as a required CI gate before merging to main.
28
— Code Quality
CI Pipeline Has No Test Requirement Or Coverage Gate
The CI configuration does not require tests to pass before a pull request can be merged. There is no coverage threshold enforced at any stage of the delivery pipeline.
Code can be merged to main and deployed to production regardless of test failures. A broken payment path or auth regression can reach production undetected if tests are skipped or failing. This has happened in similar codebases and resulted in production incidents.
Add a required CI check that runs the full test suite and blocks merge on failure. Set a minimum coverage threshold (start at current baseline, enforce it does not regress). Add a branch protection rule to main that requires the CI check to pass.
29
— Delivery
No Gated Release Process — Any Commit Can Reach Production
There is no required review, approval, or automated check gate between a commit and a production deploy. Pushes to main deploy directly.
A single unreviewed commit can introduce a payment bug or security regression into production. For an app handling real payment data and user credentials, this is a material risk — particularly given the existing security findings in this report.
Add a branch protection rule requiring at least one pull request review before merging to main. Add required CI status checks (lint, type check, tests) as merge prerequisites. Consider a deploy approval step for changes that touch payment or auth paths.
30
— Delivery
No Rollback Process For Bad Production Deploys
There is no documented procedure for rolling back a bad deploy, and the deployment configuration does not support one-command rollback.
When a bad deploy reaches production, the recovery path is to push a fix forward — which takes time and risks introducing new issues under pressure. For payment or auth regressions, every minute of exposure increases financial and reputational risk.
Configure the hosting platform to retain the last N successful builds for one-command rollback. Document the rollback procedure in the incident runbook. Test it — run a practice rollback in staging before you need it in production.
31
— Documentation
No Canonical Deployment Or Secret Rotation Runbook
There is no single authoritative document covering how to deploy, how to rotate secrets, or how to recover the system — the information exists in scattered notes, commit messages, and tribal knowledge.
A new contributor or the original developer under pressure cannot safely deploy or rotate credentials without risk of misconfiguration. Secret rotation — which is required immediately for the credentials found in SEC-002 — cannot be completed safely without a documented procedure.
Create a RUNBOOK.md covering: deployment steps, required environment variables and where they come from, secret rotation procedure for each credential class, and database recovery steps. Link it from the README. Keep it updated as the system changes.
32
— Code Quality
Revenue and Entitlement Logic Duplicated Across Multiple API Routes
Pricing, product identifiers, purchase verification, and entitlement checks are implemented separately in multiple API routes and in client-side code. There is no shared source of truth for what constitutes a valid purchase or entitlement — changes to the revenue path require coordinated edits across several files.
Duplicate business logic in the revenue and entitlement path increases the probability of inconsistency bugs: a refund that unrecords in one route but not another, or an entitlement check that correctly rejects in the primary flow but passes in a secondary verification path. Each divergence is a potential billing or access-control defect. These bugs typically only surface when a user reports an incorrect charge or accesses content they should not, making them difficult to catch in normal testing.
Centralize product definitions, entitlement checks, pack-number assignment, and purchase-recording in shared server-side helpers. Refactor the webhook and verification routes to call the same idempotent purchase-recording function. Move product metadata to a single server-side config so client analytics consume it rather than maintaining parallel hard-coded values. After centralizing, add server-side tests for the purchase and entitlement paths that cover the primary flow, webhook reconciliation, and refund handling.
33
— Code Quality
Scattered Console Logging Without Structured Output or Redaction Rules
Production API and frontend paths use direct console.log, console.warn, and console.error calls throughout, including in auth, purchase, group, and analytics flows. The current approach mixes debugging output with operational events without structured severity levels or redaction rules for user identifiers.
Console-based logging in production makes it difficult to reliably monitor errors, distinguish operational events from debug noise, or maintain an audit trail for sensitive flows. User identifiers and internal state that appear in console output may be captured by log aggregators without redaction. When a production incident occurs in the purchase or auth path, the lack of structured events with request IDs makes diagnosis significantly slower.
Add a small logging wrapper for API routes that includes severity level, event name, and explicit redaction for user identifiers and payment tokens. Remove or guard behind a development flag the frontend debug logs that leak internal state. For the purchase and auth flows specifically, replace console calls with structured events that carry a requestId so a full request trace can be reconstructed from logs.