Practical AI infrastructure
How to Build a Private CRM with Gmail and iMessage on Hetzner
A nontechnical, AI-assisted guide to hosting Twenty, connecting Gmail properly, and bringing iMessage into customer records without turning your Mac or databases into public services.
Most CRM setup guides assume you already speak Linux, Docker, DNS, OAuth, and networking. This one assumes you would rather use an AI coding agent as a technical guide while keeping control of the important decisions.
The goal is a private CRM where Gmail uses Twenty's native connection and iMessage appears inside the relevant Person record. The CRM keeps messages in its databases, while a Mac running BlueBubbles acts as the relay to the Messages network.
The important reality check
Hetzner can host the CRM and bridge, but it cannot replace the Mac. Sending or receiving iMessages still requires a Mac signed into the intended identity. If that Mac is offline, historical CRM data should remain available, but live iMessage sync and send will pause.
What runs where
Hetzner Cloud
Twenty, its background worker, PostgreSQL, Redis, HTTPS, the iMessage bridge, and a second database that retains accepted message events.
Your Mac
Messages, BlueBubbles, and Tailscale. The Mac remains private and talks to the bridge over the encrypted Tailscale network.
Google Cloud
The OAuth application that lets each user grant Twenty the approved Gmail, profile, and optional Calendar access.
GitHub
The private implementation, automated tests, review history, and optional deployment pipeline.
Why the bridge stays separate from the CRM
We do not pretend iMessage is email and we do not fork Twenty core. Twenty owns CRM records, permissions, Gmail, and the embedded interface. A separate bridge owns BlueBubbles webhooks, contact matching, duplicate protection, offline storage, and sending.
That boundary matters. It lets you update or replace one side without burying provider passwords in front-end code or making the CRM responsible for Apple-specific behaviour.
Start by giving your AI guardrails
An agent that can run commands is useful, but it should not make purchases, expose ports, grant mailbox access, or send messages without you. Paste this at the beginning of the task.
You are helping a nontechnical owner deploy a private CRM on Hetzner Cloud.
Work one phase at a time. Inspect the current repository and infrastructure before changing anything.
Safety rules:
1. Never print, paste into chat, commit, or log passwords, tokens, OAuth secrets, database credentials, or .env contents.
2. Ask immediately before purchases, DNS changes, OAuth grants, production configuration, database migrations, deletions, restores, or outbound messages.
3. Do not expose application, bridge, database, or Redis ports to the public internet. Only the HTTPS reverse proxy should be public.
4. Keep BlueBubbles private over Tailscale. Do not disable SIP or enable the BlueBubbles Private API.
5. Keep the iMessage bridge separate from Twenty. Do not fork or edit Twenty core.
6. Do not import old iMessage history automatically. Preview it and persist only the messages I select.
7. Do not send a test email or iMessage until I confirm the sender, recipient, and exact content.
8. Preserve existing data. Take and verify a backup before risky work.
9. Use pinned, tested versions first. Treat upgrades as separate work.
10. End every phase with evidence: what changed, checks run, current status, and what remains pending.
If you cannot inspect the browser, terminal, repository, or live service, tell me exactly what I must do and wait for the result. Never pretend a step worked.Phase 1: Build the Hetzner foundation
Use a real domain, an Ubuntu LTS server, SSH keys, a Hetzner firewall, and automatic backups. Two gigabytes of RAM is Twenty's published minimum, but this stack runs more than Twenty. Start with at least 4 GB and prefer 8 GB if you expect meaningful email imports or more than one user.
Public traffic should stop at the HTTPS reverse proxy. Bind the Twenty service itself to127.0.0.1:3000. Do not expose the bridge, PostgreSQL, or Redis ports.
Guide me through deploying this system on Hetzner Cloud.
Use an x86 Ubuntu LTS server with enough memory for Twenty, its worker, two PostgreSQL databases, Redis, Caddy, and the iMessage bridge. Check current Hetzner plans and prices before recommending a server.
Create a firewall plan that allows public TCP 80 and 443, restricts SSH to a trusted source during setup, and never exposes ports 3000, 3100, 5432, 5433, or 6379.
Install Docker Engine, Docker Compose, Git, security updates, and Tailscale from official sources. Use a non-root deploy user and SSH keys. Disable password login only after proving key login in a second session.
Deploy with a real domain and HTTPS. Keep secrets in a protected server-side environment file with permissions 0600. Configure restart policies, health checks, daily application-consistent database dumps, off-server backups, and a documented rollback.
Show me the exact purchase, firewall, DNS, and production changes and ask for confirmation before applying them.Backups need two layers
Hetzner's automatic server backups are a useful recovery point. They are not a substitute for application-consistent PostgreSQL dumps and file-storage archives copied off the server. Hetzner also notes that attached Volumes are not included in server backups or snapshots.
Phase 2: Prepare the Mac without making it public
Install BlueBubbles on a Mac that can remain powered on and signed into Messages. The safest route is to join the Mac and Hetzner server to the same Tailscale network, then allow the bridge port only on the Tailscale interface.
Keep System Integrity Protection enabled. The BlueBubbles Private API is not needed for the standard text flow described here. Also check what really happens after a power outage: BlueBubbles can start at login, but FileVault or the macOS login screen may still require a human to unlock the Mac after a cold restart.
Guide me through preparing a dedicated Mac for the iMessage connection.
Confirm Messages is signed into the intended identity. Install the tested BlueBubbles Server version and Tailscale using their official instructions. Grant only required permissions.
Keep SIP enabled and the BlueBubbles Private API disabled. Do not use a public proxy for CRM traffic. Connect the Mac to the Hetzner server only through Tailscale.
Configure BlueBubbles and Tailscale to start at login, prevent sleep during operation, and explain whether FileVault or the login screen still requires a person after a cold reboot. Do not weaken encryption or enable automatic login without separate approval.
Prove receive and send outside the CRM first. Ask me to confirm the exact recipient and message immediately before the test send.Phase 3: Connect Gmail the deliberate way
Gmail is not just a client ID and secret. You need the correct APIs, OAuth audience, authorised domain, redirect URI, consent screen, and workspace policy. For a self-hosted Twenty instance, the callback is:
https://<CRM_DOMAIN>/auth/google-apis/get-access-tokenBefore connecting a mailbox, decide whether to import everything or selected folders, whether group addresses are excluded, what teammates can see, and when new People records are created. Those privacy choices are harder to undo after thousands of emails have been imported.
Configure Gmail for the self-hosted Twenty instance at https://<CRM_DOMAIN>.
Use a dedicated Google Cloud project, or inspect an existing project and explain the impact before changing it. Enable the Gmail, Google Calendar, and People APIs. Configure OAuth branding, audience, authorised domains, and a Web application client with this callback:
https://<CRM_DOMAIN>/auth/google-apis/get-access-token
Enter the OAuth secret only into a masked protected field. Before saving Google Cloud or Twenty production settings, list the exact variables and requested scopes and ask for confirmation.
Help me choose email folders, group-address exclusions, teammate visibility, contact auto-creation, and internal-email handling before starting sync.
Complete the OAuth flow promptly. Do not claim success at “Account added” or “Importing”. Wait for “Synced”, inspect worker errors, and verify a bounded sample of expected email. Do not send a test email without separate confirmation.“Importing” is not the finish line
A successful Google consent screen proves permission was granted. It does not prove the mailbox finished syncing. Keep the task open until Twenty shows Synced, worker jobs are clean, and a bounded sample of expected mail is visible.
Phase 4: Link iMessage to the right Person
Match conversations using normalised phone numbers or Apple ID email addresses. If two CRM records match the same address, stop for manual resolution instead of guessing or merging people automatically.
New webhook messages can be retained automatically. Old history should be different: preview a narrow date range, select individual messages, and import only those selections. That keeps someone's personal history from being silently copied into a business system.
Adding more iMessage accounts
Use a separate BlueBubbles server for each Mac or Apple identity, and give each one a stable account key. Store every password encrypted, return account lists without credentials, and use account-specific webhook paths. Test account isolation before any outbound send.
Phase 5: Make operations boring
Long-term reliability comes from mundane controls: restart policies, pinned versions, health checks, backups, restore tests, CI before deployment, and a written reboot procedure.
- Use
restart: unless-stoppedfor long-running Docker services. - Start Tailscale with the operating system and BlueBubbles when the Mac user logs in.
- Keep the production environment file off GitHub and readable only by the deploy user.
- Run tests and a deployment preview before changing production.
- Test restoration into isolated databases instead of trusting an upload notification.
- Verify the actual commit and user flow after every release.
Final acceptance prompt
This last prompt forces the agent to distinguish “services are running” from “the system works.”
Run a final acceptance test without making unapproved changes.
Verify the HTTPS domain, invite-only access, intended CRM members, healthy services, current deployed version, Gmail status, Tailscale privacy, BlueBubbles security settings, inbound iMessage matching, offline retention, selective history import, multiple-account isolation, workflow idempotency, off-server backups, and restore evidence.
Do not call the system finished because containers are running. Report each user-facing test as passed, failed, pending, or not in scope, with evidence. Do not send anything unless I separately confirm the sender, recipient, and content.What good looks like
- The CRM loads at a real HTTPS domain.
- Access is invite-only and only intended members remain.
- Gmail reaches Synced under the chosen privacy settings.
- The Mac and Hetzner communicate privately over Tailscale for BlueBubbles traffic.
- A consenting inbound iMessage reaches the correct Person record.
- A separately confirmed outbound message appears once, not twice.
- Existing messages remain visible when the Mac is offline.
- Historical messages are imported only after individual selection.
- Daily off-server backups exist and a test restore works.
- Reboot behaviour is tested and any required human login is documented.
Official references
- Hetzner: Creating a server
- Hetzner: Creating a firewall
- Hetzner: Backups and snapshots
- Twenty: Self-host with Docker Compose
- Tailscale: Install on Linux
- BlueBubbles: Manual server setup
Want help designing the safe version for your business?
WhiteHorse AI designs and operates practical AI systems around your existing team, data, permissions, and approval boundaries.
Talk to WhiteHorse AI