feat: add Twenty CRM /crm route + deploy stack for commons-hub lead funnel

Adds dedicated /crm sub-route to rNetwork module embedding Twenty CRM
via ExternalAppShell iframe. Updates TWENTY_API_URL to use internal Docker
networking (http://twenty-server:3000). Includes full Twenty CRM Docker
stack (server, worker, postgres, redis) with Traefik routing for
crm.rspace.online and deployment instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-03 13:53:50 -08:00
parent 80e42596b3
commit 07e53d6aa1
5 changed files with 285 additions and 1 deletions

View File

@ -0,0 +1,10 @@
# Twenty CRM secrets
# Generate these before first deploy:
# APP_SECRET: openssl rand -hex 32
# POSTGRES_PASSWORD: openssl rand -hex 16
POSTGRES_PASSWORD=changeme
APP_SECRET=changeme
# Store these in Infisical (twenty-crm project) for production.
# The .env file is only used for initial bootstrap / local dev.

123
deploy/twenty-crm/DEPLOY.md Normal file
View File

@ -0,0 +1,123 @@
# Twenty CRM Deployment — commons-hub Lead Funnel
## 1. Deploy Twenty CRM Stack on Netcup
```bash
# SSH to server
ssh netcup-full
# Create directory and copy files
mkdir -p /opt/twenty-crm
# (copy docker-compose.yml and .env from this directory)
# Generate secrets
cd /opt/twenty-crm
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 16)
APP_SECRET=$(openssl rand -hex 32)
EOF
# Start the stack
docker compose up -d
# Wait for healthy status (may take 1-2 minutes on first boot)
docker compose ps
docker logs twenty-server --tail 50
```
## 2. Create Admin Account
Once Twenty is healthy at `https://crm.rspace.online`:
1. Open `https://crm.rspace.online` in browser
2. Twenty will show the initial setup wizard
3. Create admin account: **jeff / jeffemmett@gmail.com**
4. Set workspace name: **commons-hub**
## 3. Generate API Token & Store in Infisical
1. In Twenty: Settings → Accounts → API Keys → Create API key
2. Copy the token
3. Store in Infisical:
- Project: `rspace` (same project as rSpace secrets)
- Secret name: `TWENTY_API_TOKEN`
- Secret value: the API token from step 2
4. Restart rSpace to pick up the new token:
```bash
cd /opt/rspace-online && docker compose restart rspace
```
## 4. Configure Lead Funnel Pipeline
In Twenty CRM UI: Settings → Data model → Opportunity → Edit stages
### Pipeline Stages (7)
| # | Stage | Color |
|---|-------|-------|
| 1 | New Lead | Blue |
| 2 | Contacted | Yellow |
| 3 | Qualified | Orange |
| 4 | Offer Sent | Purple |
| 5 | Confirmed | Teal |
| 6 | Won | Green |
| 7 | Lost / Not Now | Red |
### Custom Fields
Add via Settings → Data model → [Object] → Add field:
**On Opportunity:**
- Event Dates (preferred) — DATE
- Event Dates (flexible range) — TEXT
- Group Size — NUMBER
- Needs: Accommodation — BOOLEAN
- Needs: Catering — BOOLEAN
- Needs: Rooms — BOOLEAN
- Needs: AV — BOOLEAN
- Next Action Date — DATE (required)
- Follow-up Date — DATE
- Lost Reason — TEXT
**On Company:**
- Lead Source — SELECT (options: Website, Referral, Event, Cold Outreach, Partner, Other)
- Last Touch Date — DATE
### Saved Views
Create via the Opportunities list → Save view:
1. **My Pipeline** — Group by: Stage, Filter: Assigned to = current user
2. **Needs Follow-up** — Filter: Next Action Date <= today
3. **Stale Leads** — Filter: Next Action Date is empty
## 5. Create commons-hub Space
This will be done via the rSpace community store or directly:
- Space slug: `commons-hub`
- Visibility: `permissioned`
- Owner: jeff (jeffemmett@gmail.com)
- Enabled modules: `["rnetwork"]`
The CRM will then be accessible at: `commons-hub.rspace.online/rnetwork/crm`
## 6. Deploy rSpace Changes
```bash
ssh netcup-full
cd /opt/rspace-online
git pull
docker compose up -d --build
```
## Verification Checklist
- [ ] `docker compose ps` on Netcup — all Twenty containers healthy
- [ ] `curl https://crm.rspace.online` — Twenty CRM loads
- [ ] Navigate to `commons-hub.rspace.online/rnetwork/crm` — CRM embedded in rSpace shell
- [ ] Log in as jeff — admin access confirmed
- [ ] Open pipeline view — 7 stages visible
- [ ] Create test opportunity — all custom fields present
- [ ] Verify "Next Action Date" is required
- [ ] Check rNetwork graph view still works (`/rnetwork` default view)

View File

@ -0,0 +1,137 @@
# Twenty CRM stack for commons-hub lead funnel
# Deploy to /opt/twenty-crm/ on Netcup
#
# Prerequisites:
# - rspace-online stack running (creates rspace-online_rspace-internal network)
# - Traefik running on traefik-public network
# - .env with INFISICAL_CLIENT_ID + INFISICAL_CLIENT_SECRET
#
# Secrets fetched from Infisical (twenty-crm project):
# POSTGRES_PASSWORD, APP_SECRET, ADMIN_PASSWORD
services:
twenty-server:
image: twentycrm/twenty:latest
container_name: twenty-server
restart: unless-stopped
depends_on:
twenty-db:
condition: service_healthy
twenty-redis:
condition: service_healthy
environment:
# ── Core ──
- NODE_ENV=production
- SERVER_URL=https://crm.rspace.online
- FRONT_BASE_URL=https://crm.rspace.online
- PORT=3000
# ── Database ──
- PG_DATABASE_URL=postgres://twenty:${POSTGRES_PASSWORD}@twenty-db:5432/twenty
# ── Redis ──
- REDIS_URL=redis://twenty-redis:6379
# ── Auth ──
- APP_SECRET=${APP_SECRET}
- ACCESS_TOKEN_SECRET=${APP_SECRET}
- LOGIN_TOKEN_SECRET=${APP_SECRET}
- REFRESH_TOKEN_SECRET=${APP_SECRET}
- FILE_TOKEN_SECRET=${APP_SECRET}
# ── Storage ──
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=.local-storage
# ── Misc ──
- SIGN_IN_PREFILLED=false
- IS_BILLING_ENABLED=false
- TELEMETRY_ENABLED=false
volumes:
- twenty-server-data:/app/.local-storage
labels:
- "traefik.enable=true"
- "traefik.http.routers.twenty-crm.rule=Host(`crm.rspace.online`)"
- "traefik.http.routers.twenty-crm.entrypoints=web"
- "traefik.http.routers.twenty-crm.priority=130"
- "traefik.http.services.twenty-crm.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-public"
networks:
- traefik-public
- rspace-internal
- twenty-internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
twenty-worker:
image: twentycrm/twenty:latest
container_name: twenty-worker
restart: unless-stopped
command: ["yarn", "worker:prod"]
depends_on:
twenty-db:
condition: service_healthy
twenty-redis:
condition: service_healthy
environment:
- NODE_ENV=production
- PG_DATABASE_URL=postgres://twenty:${POSTGRES_PASSWORD}@twenty-db:5432/twenty
- REDIS_URL=redis://twenty-redis:6379
- APP_SECRET=${APP_SECRET}
- ACCESS_TOKEN_SECRET=${APP_SECRET}
- LOGIN_TOKEN_SECRET=${APP_SECRET}
- REFRESH_TOKEN_SECRET=${APP_SECRET}
- FILE_TOKEN_SECRET=${APP_SECRET}
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=.local-storage
- SERVER_URL=https://crm.rspace.online
- TELEMETRY_ENABLED=false
volumes:
- twenty-server-data:/app/.local-storage
networks:
- twenty-internal
twenty-db:
image: postgres:16-alpine
container_name: twenty-db
restart: unless-stopped
environment:
- POSTGRES_DB=twenty
- POSTGRES_USER=twenty
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- twenty-pgdata:/var/lib/postgresql/data
networks:
- twenty-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U twenty -d twenty"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
twenty-redis:
image: redis:7-alpine
container_name: twenty-redis
restart: unless-stopped
volumes:
- twenty-redis-data:/data
networks:
- twenty-internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
twenty-server-data:
twenty-pgdata:
twenty-redis-data:
networks:
traefik-public:
external: true
rspace-internal:
name: rspace-online_rspace-internal
external: true
twenty-internal:

View File

@ -38,7 +38,7 @@ services:
- IMAP_HOST=mail.rmail.online
- IMAP_PORT=993
- IMAP_TLS_REJECT_UNAUTHORIZED=false
- TWENTY_API_URL=https://rnetwork.online
- TWENTY_API_URL=http://twenty-server:3000
- OLLAMA_URL=http://ollama:11434
- INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID}
- INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET}

View File

@ -214,6 +214,20 @@ routes.get("/api/workspaces", (c) => {
]);
});
// ── CRM sub-route — dedicated iframe to Twenty CRM ──
routes.get("/crm", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderExternalAppShell({
title: `${space} — CRM | rSpace`,
moduleId: "rnetwork",
spaceSlug: space,
modules: getModuleInfoList(),
appUrl: "https://crm.rspace.online",
appName: "Twenty CRM",
theme: "dark",
}));
});
// ── Page route ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";