What you actually own, and what you are actually responsible for.
This page describes how the shipped package works. Everything here is limited to what the implementation does today. Where a claim would be convenient but unearned — certifications, guaranteed isolation, security by virtue of self-hosting — it is not made.
Hosting architecture
One Next.js application and one database file, running in a container you start.
The package ships as a Next.js application with a Docker Compose file. It runs as a single container that binds to a local port on your host. Nothing about the runtime phones home, and there is no vendor control plane that can switch your install off.
The reference deployment puts the container behind a Cloudflare Tunnel so the port is never exposed publicly, but the app is an ordinary Node service — a VPS, a home server, or a cloud instance all work. Public surfaces (hosted forms, booking pages, live client sites, link pages, redirect routes, Stripe webhooks, and voice webhooks) are served by the same app and are deliberately excluded from the admin access policy.
Buyer-owned accounts and credentials
Every third-party account in the chain is registered in your name, not resold through us.
Hosting, domain registration, Cloudflare, Stripe, and Resend are all accounts you create and control. We never hold your API keys, and there is no shared tenant that your workspace lives inside.
Secrets are read from environment variables — in the reference deploy, a root-owned 0600 env file on the host. They are not stored in the database and not committed to the repository. Rotating a key means editing that file and restarting the container.
Data storage and backups
A single SQLite file on a volume you control, with a backup script and documented restore steps.
The CRM stores everything in one SQLite database file — contacts, deals, activities, tasks, forms, bookings, products, quotes, sites, and link pages. It runs in WAL mode so reads keep working while a write is in flight. The schema is applied idempotently on first connection.
Because it is one file, backup and restore are ordinary file operations. The package includes a backup script and the documented restore path: stop the container, extract the snapshot, replace the database file, start it again. There is no export request to file and no waiting on a vendor to release your data.
Backups only exist if you schedule them. The cron entry is in the setup guide; running and verifying it is the buyer's job.
Workspace separation
Records are scoped to a workspace. Treat that as organization, not as a hard security boundary between hostile tenants.
An agency install has an agency layer above client sub-accounts. Contacts, deals, forms, and the rest are stored against a workspace, so one client's records do not appear in another client's lists.
Being straight about the limit: this is designed for an agency running workspaces on behalf of its own clients, where every logged-in user is on your team and vetted by you. It is not audited multi-tenant isolation for mutually untrusting tenants who each get their own login. If you need that, plan on strengthening the workspace boundary in the source before selling seats into it.
Cloudflare Access
Identity is enforced at the edge, which means the edge has to be the only way in.
In production the dashboard and platform routes have no password of their own. Cloudflare Access authenticates the user against your policy and passes a verified email header, which the application trusts to resolve identity. The first authenticated email on a fresh database is promoted to owner.
This is the most important security consequence of the design: because the app trusts that header, the container must not be reachable except through the tunnel. If the port is exposed directly to the internet, anyone able to set the header can reach the dashboard. Keep the port bound locally, keep the Access policy attached to the admin paths, and keep the public form, booking, site, link, billing, and voice paths on the bypass list so buyers and clients can still reach them.
Stripe payment handling
Checkout runs against your Stripe account. The CRM records the outcome; it is not the merchant of record.
You connect your own Stripe keys. Checkout and webhook routes are public by necessity and verify what Stripe sends before recording anything. Card data never touches the CRM database — Stripe holds it, and the CRM stores the record of the purchase.
Quote and invoice records track line items, discounts, tax, terms, and status. Collecting the money happens through your Stripe account. There is no in-dashboard card form today, and this page will not pretend there is one.
Resend email configuration
Optional, buyer-configured, and deliberately non-blocking.
New-lead notifications send through Resend using your API key and your verified sender domain. Without a key configured, the CRM logs the notification and carries on — form ingest still writes the contact, deal, and activity.
Notification sending is fire-and-forget by design. A slow or failing email provider must never delay or fail the public form response, because that response is what the person filling out the form is waiting on.
Domain and DNS ownership
The domain is registered to you, and the DNS records point wherever you decide.
You buy the domain and hold the registrar login. The setup guide covers pointing it at your install and adding the hostname to your tunnel configuration. Moving the CRM to different infrastructure later is a DNS change plus a container start — there is no vendor-controlled subdomain to migrate off.
Updates and maintenance
You choose when to pull changes. Nothing updates underneath you.
Because the install is yours, no release lands on your production system without you running it. Updating means pulling the repository and rebuilding the container. That also means security patches are on your schedule, not ours — the flip side of nobody being able to break your workflow with a surprise redesign.
The optional Care Plan exists for buyers who would rather hand off updates, monitoring, and backup checks. It is a service, not a license requirement; the software keeps working if you cancel it.
Your security responsibilities
Self-hosting moves control to you. It also moves the work to you.
Self-hosting does not make software secure by itself, and CRM Stack carries no security certification — no SOC 2, no ISO 27001, no HIPAA attestation. Anyone telling you that running your own server is automatically safer than a hosted vendor is selling something.
- Keep the host patched and restrict SSH access.
- Keep the app port bound to localhost and reachable only through the tunnel.
- Keep the Access policy limited to the emails that actually need the dashboard, and remove people when they leave.
- Store secrets in a root-owned file with restrictive permissions, and rotate keys when someone with access departs.
- Run the backup job, and restore from a snapshot at least once so you know the path works before you need it.
- Keep the form kill-switch in mind: deactivating a form stops its public endpoint immediately.
- Understand your own obligations for the customer data you collect — that liability is yours, not the vendor's.
Extending the source code safely
You hold the code. A few load-bearing paths are worth understanding before you change them.
The codebase is meant to be extended with Claude, Codex, or a developer. It ships with a test suite and a typecheck script; run both before deploying a change.
- Lead ingest is the revenue path. The contact upsert is load-bearing — if it fails the submission should error. Deal and activity writes are logged as non-fatal so a partial failure still captures the customer.
- Do not gate the public form paths. Putting the hosted forms or their submit endpoint behind the access policy silently stops lead capture.
- Do not block the form response on email. Notifications are fire-and-forget for a reason.
- Keep the pipeline foreign key. Deals reference a composite key on stage and pipeline, so a UI bug cannot move a deal into another pipeline's stage.
- Keep the form active check. It is what makes deactivation a real kill switch.
The repository documents these rules alongside the code, so a coding agent working in the project picks them up without you having to re-explain them each session.