Security
What is enforced, and where
A check that exists only in the interface is not a check. Everything below is enforced on the server.
Access is scoped on the server
- Every account is approved for named states. That approval lives in a database table keyed to the company, never on a user record, so no user setting can widen it.
- The state on a request is intersected with the approved set, never unioned. Editing a URL, a form field or an API parameter cannot return a program from an unapproved state.
- Ownership is checked on every row read: a check id belonging to another company returns a 403.
Customer data we deliberately do not hold
- Customer names are not collected.
- Street addresses are not persisted. Where an address is submitted for territory verification it is used within the request and discarded; it is not written to the database, to logs, to analytics or to error monitoring.
- Utility account numbers are not stored.
- Audit metadata is scrubbed on write — address, name, account number, EIN, password and token keys are replaced before the row is saved.
Authentication
- Passwords are hashed with scrypt and a per-password salt.
- Sessions are opaque random tokens. What is stored is an HMAC of the token, so a database dump contains no usable session.
- Session cookies are HttpOnly, SameSite=Lax, and Secure in production.
- Repeated failed sign-ins lock the account temporarily, and sign-in is rate limited per network.
Payments
- Card details are entered on Stripe's hosted checkout. Punchlist never receives or stores a card number.
- Subscription state changes only in response to a Stripe webhook whose signature is verified server-side against the raw request body.
- Webhook events are recorded by event id and processed once, so a replayed event changes nothing.
- Frontend payment state is never trusted.
Secrets
- Database credentials, the session secret and Stripe secret keys are read only in server modules that are marked server-only, which makes importing them into browser code a build failure.
- No secret is embedded in a client bundle.