86d ships with strict defaults across every layer that touches the network or user data. This page is a reference for what those defaults are, where each protection lives, and what knobs you have.Documentation Index
Fetch the complete documentation index at: https://86d.app/docs/llms.txt
Use this file to discover all available pages before exploring further.
Authentication
- Better Auth. Sessions are signed with
BETTER_AUTH_SECRET. The placeholder in.env.exampleis rejected by the boot validator; you must replace it with a 32-byte random string. Generate withopenssl rand -base64 32. - Bcrypt for passwords. Passwords are hashed with bcrypt and never stored in plaintext.
- Cookies are HTTP-only and SameSite-Lax. Session tokens cannot be read by JavaScript.
- OAuth callbacks use
BETTER_AUTH_URL. Misconfigured callback URLs fail closed at the provider. - 86d.app SSO. When
86D_API_KEYis set, admin authentication delegates to 86d.app’s identity provider.
Admin access
- The admin app at
/adminis gated by anadminrole check. - Unauthenticated requests redirect to
/auth/signin. - The default seeded admin (
admin@example.com/password123) must be replaced before any internet-facing deploy.
API rate limits
Rate limits are enforced per route class:| Surface | Limit | Identifier |
|---|---|---|
| Public storefront API | 120 req / minute | IP address |
| Sensitive public endpoints (newsletter subscribe, payment intent create) | 10 req / 10 minutes | IP address |
Admin API (/api/admin/...) | 300 req / minute | userId |
HTTP 429 with Retry-After and X-RateLimit-Reset headers.
File uploads
POST /api/upload is admin-only. Every uploaded file is validated against:
- Magic-byte detection (the actual file format), not the
Content-Typeheader. - A size cap per format (4.5 MB for images, 10 MB for PDFs).
- An SVG sanitizer that rejects embedded scripts, event handlers, and
javascript:URIs.
stores/{storeId}/{uuid} so cross-store reads, writes, and deletes are not possible even with a forged path. SVG responses use a strict CSP (default-src 'none'; style-src 'unsafe-inline'); PDFs are served as attachments.
DELETE /api/upload includes a store-isolation check so admins of one store cannot delete files owned by another.
See Storage configuration.
Webhook verification
Every payment provider module implements HMAC signature verification before any handler runs:- Raw body capture. The signature is computed against the raw request body, not the JSON-parsed value.
- HMAC-SHA256 with a constant-time comparator. No external crypto library; verification runs on the Web Crypto API.
- Replay protection. Webhook timestamps older than 5 minutes are rejected with
401.
Multi-tenancy
Every database row that is store-scoped includes astoreId foreign key. Every admin endpoint resolves storeId from the session, never from the request body. Every storage path is prefixed with stores/{storeId}/. Stores cannot read or write each other’s data.
When you set STORE_ID and 86D_API_KEY, the store fetches its config from the hosted 86d API. The hosted API authenticates using the API key, scopes responses to the store, and refuses to return any other store’s configuration.
Database
- Prisma 7 with parameterized queries. SQL injection is structurally impossible from app code.
- Migrations run with
DATABASE_URL_UNPOOLEDto avoid PgBouncer issues. - Module data access is funneled through
ModuleDataService, which scopes every query to the calling module’s schema. A module cannot read another module’s tables directly.
Identity at the request boundary
The storefront never accepts a customer’s identity from a request body. On every authenticated request, the customer is taken from the session cookie. Admin endpoints likewise do not trust client-supplied user IDs.Secrets
BETTER_AUTH_SECRET, payment provider keys, and webhook secrets are read from environment variables.- They are never logged or returned in API responses.
redactUrl()is used when printingDATABASE_URLto console (init,dev,doctor); only host:port is shown.
What you should do before going live
- Replace
BETTER_AUTH_SECRETwith a freshopenssl rand -base64 32. - Change the default admin password.
- Set
BETTER_AUTH_URLandAPP_URLto your real domain. - Configure a payment provider with the appropriate webhook secret.
- Run
86d doctorand resolve every fail. - Enable Sentry by setting
SENTRY_DSNso production errors are captured. - Enable
@86d-app/audit-logif you want a tamper-evident record of admin actions.

