Skip to content

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.