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:
parent
80e42596b3
commit
07e53d6aa1
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -38,7 +38,7 @@ services:
|
||||||
- IMAP_HOST=mail.rmail.online
|
- IMAP_HOST=mail.rmail.online
|
||||||
- IMAP_PORT=993
|
- IMAP_PORT=993
|
||||||
- IMAP_TLS_REJECT_UNAUTHORIZED=false
|
- IMAP_TLS_REJECT_UNAUTHORIZED=false
|
||||||
- TWENTY_API_URL=https://rnetwork.online
|
- TWENTY_API_URL=http://twenty-server:3000
|
||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
- INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID}
|
- INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID}
|
||||||
- INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET}
|
- INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET}
|
||||||
|
|
|
||||||
|
|
@ -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 ──
|
// ── Page route ──
|
||||||
routes.get("/", (c) => {
|
routes.get("/", (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue