Initial resurrection kit — 81 scripts, 62 references, configs, systemd units, crons, Docker compose files, Caddy config, master README
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
docuseal:
|
||||
image: docuseal/docuseal:latest
|
||||
container_name: docuseal
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- HOST=http://localhost:3000
|
||||
- FORCE_SSL=false
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
mongo:
|
||||
image: mongo
|
||||
restart: unless-stopped
|
||||
command: --quiet --wiredTigerCacheSizeGB 0.25
|
||||
volumes:
|
||||
- ./data/mongo-data:/data/db
|
||||
- ./data/mongo-config:/data/configdb
|
||||
networks:
|
||||
- komodo-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
|
||||
core:
|
||||
image: ghcr.io/moghtech/komodo-core:2
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9120:9120
|
||||
depends_on:
|
||||
- mongo
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./keys:/config/keys
|
||||
environment:
|
||||
- KOMODO_DATABASE_ADDRESS=mongo:27017
|
||||
- KOMODO_PORT=9120
|
||||
- KOMODO_HOST=https://komodo.itpropartner.com
|
||||
- KOMODO_TITLE=Komodo
|
||||
- KOMODO_LOCAL_AUTH=true
|
||||
- KOMODO_INIT_ADMIN_USERNAME=admin
|
||||
- KOMODO_INIT_ADMIN_PASSWORD=changeme
|
||||
- KOMODO_FIRST_SERVER_NAME=core
|
||||
- KOMODO_PERIPHERY_PUBLIC_KEY=file:/config/keys/periphery.pub
|
||||
- KOMODO_DISABLE_CONFIRM_DIALOG=false
|
||||
- KOMODO_DISABLE_INIT_RESOURCES=false
|
||||
- KOMODO_WEBHOOK_SECRET=komodo-webhook-secret
|
||||
- KOMODO_JWT_SECRET=komodo-jwt-secret-change-me
|
||||
- KOMODO_JWT_TTL=1-day
|
||||
- KOMODO_MONITORING_INTERVAL=15-sec
|
||||
- KOMODO_RESOURCE_POLL_INTERVAL=1-hr
|
||||
- KOMODO_DISABLE_USER_REGISTRATION=false
|
||||
- KOMODO_ENABLE_NEW_USERS=false
|
||||
- KOMODO_TRANSPARENT_MODE=false
|
||||
networks:
|
||||
- komodo-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
|
||||
networks:
|
||||
komodo-net:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,33 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
openwebui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: openwebui
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- openwebui_data:/app/backend/data
|
||||
environment:
|
||||
- WEBUI_SECRET_KEY=your_secret_key
|
||||
restart: unless-stopped
|
||||
|
||||
litellm:
|
||||
build: ../litellm
|
||||
container_name: litellm
|
||||
ports:
|
||||
- "4000:4000"
|
||||
volumes:
|
||||
- litellm_data:/app/data
|
||||
restart: unless-stopped
|
||||
|
||||
mcp-email:
|
||||
build: ../mcp-email
|
||||
container_name: mcp-email
|
||||
ports:
|
||||
- "8902:8902"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
openwebui_data:
|
||||
litellm_data:
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8902"]
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi
|
||||
fastmcp>=3.0
|
||||
uvicorn
|
||||
python-dotenv
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8900"]
|
||||
@@ -0,0 +1,3 @@
|
||||
fastapi>=0.68.0
|
||||
uvicorn>=0.15.0
|
||||
python-multipart>=0.0.5
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server.py .
|
||||
|
||||
CMD ["python", "server.py"]
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi>=0.68.0
|
||||
uvicorn>=0.15.0
|
||||
requests>=2.26.0
|
||||
fastmcp>=3.0.0
|
||||
@@ -0,0 +1,26 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
searxng:
|
||||
image: searxng/searxng:latest
|
||||
container_name: searxng
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:8888:8080"
|
||||
volumes:
|
||||
- ./searxng-data:/etc/searxng:rw
|
||||
- ./searxng-themes:/usr/local/searxng/searx/static/themes:rw
|
||||
environment:
|
||||
- SEARXNG_BASE_URL=http://localhost:8080
|
||||
- SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY:-changeme}
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- DAC_OVERRIDE
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1 @@
|
||||
EXA_API_KEY=d1720047-10bb-47d5-95a3-74e6eabeea16
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start development environment (frontend + backend + worker)
|
||||
yarn start
|
||||
|
||||
# Individual package development
|
||||
npx nx start twenty-front # Start frontend dev server
|
||||
npx nx start twenty-server # Start backend server
|
||||
npx nx run twenty-server:worker # Start background worker
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Preferred: run a single test file (fast)
|
||||
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
|
||||
# Run all tests for a package
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
# To run an individual test or a pattern of tests, use the following command:
|
||||
cd packages/{workspace} && npx jest "pattern or filename"
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
npx nx storybook:test twenty-front
|
||||
|
||||
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting (diff with main - fastest, always prefer this)
|
||||
npx nx lint:diff-with-main twenty-front
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
|
||||
|
||||
# Linting (full project - slower, use only when needed)
|
||||
npx nx lint twenty-front
|
||||
npx nx lint twenty-server
|
||||
|
||||
# Type checking
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
|
||||
# Format code
|
||||
npx nx fmt twenty-front
|
||||
npx nx fmt twenty-server
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build packages (twenty-shared must be built first)
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```bash
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
packages/
|
||||
├── twenty-front/ # React frontend application
|
||||
├── twenty-server/ # NestJS backend API
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website/ # Next.js marketing website
|
||||
├── twenty-docs/ # Documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
### Key Development Principles
|
||||
- **Functional components only** (no class components)
|
||||
- **Named exports only** (no default exports)
|
||||
- **Types over interfaces** (except when extending third-party interfaces)
|
||||
- **String literals over enums** (except for GraphQL enums)
|
||||
- **No 'any' type allowed** — strict TypeScript enforced
|
||||
- **Event handlers preferred over useEffect** for state updates
|
||||
- **Props down, events up** — unidirectional data flow
|
||||
- **Composition over inheritance**
|
||||
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
|
||||
|
||||
### Naming Conventions
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
|
||||
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
|
||||
- **TypeScript generics**: descriptive names (`TData` not `T`)
|
||||
|
||||
### File Structure
|
||||
- Components under 300 lines, services under 500 lines
|
||||
- Components in their own directories with tests and stories
|
||||
- Use `index.ts` barrel exports for clean imports
|
||||
- Import order: external libraries first, then internal (`@/`), then relative
|
||||
|
||||
### Comments
|
||||
- Use short-form comments (`//`), not JSDoc blocks
|
||||
- Explain WHY (business logic), not WHAT
|
||||
- Do not comment obvious code
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
### Backend Architecture
|
||||
- **NestJS modules** for feature organization
|
||||
- **TypeORM** for database ORM with PostgreSQL
|
||||
- **GraphQL** API with code-first approach
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Upgrade Commands
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
### Testing Strategy
|
||||
- **Test behavior, not implementation** — focus on user perspective
|
||||
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
|
||||
- Query by user-visible elements (text, roles, labels) over test IDs
|
||||
- Use `@testing-library/user-event` for realistic interactions
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, copies `.env` files, and initializes the database schema (runs migrations) on a fresh database. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Detailed development guidelines and best practices
|
||||
@@ -0,0 +1,159 @@
|
||||
# Twenty Website — DESIGN.md
|
||||
|
||||
> Visual system for the Twenty marketing site. Distilled from `packages/twenty-website/src/theme/`. Loaded by every `impeccable` invocation alongside PRODUCT.md.
|
||||
|
||||
## Theme
|
||||
|
||||
**Light by default.** A founder browsing a partner profile in daylight on a 14–27 inch monitor is the default scene. The site does ship a `data-scheme="dark"` override (see `css-variables.ts`), but no current public page opts into it. Treat dark as a deferred surface.
|
||||
|
||||
## Color
|
||||
|
||||
Palette is OKLCH-equivalent neutrals at the surface level. The brand accents (blue, pink, yellow, green) are present in the token system but used sparingly — none of them appear on the partner pages.
|
||||
|
||||
### Strategy: Restrained
|
||||
|
||||
Tinted neutrals + one accent ≤10%. The accent for partner pages is the deep ink black (`var(--color-black-100)`) used in CTAs and hover states. Anything beyond a hairline border, an icon glyph, or a primary CTA should question whether it needs color at all.
|
||||
|
||||
### Tokens (from `src/theme/colors.ts` + `css-variables.ts`)
|
||||
|
||||
Neutrals (the workhorses):
|
||||
|
||||
| Token | Hex (computed) | Role |
|
||||
| --- | --- | --- |
|
||||
| `colors.primary.background[100]` | `#ffffff` | Page + card surface |
|
||||
| `colors.primary.text[100]` | `#1c1c1c` | Headlines, primary text |
|
||||
| `colors.primary.text[80]` | `#1c1c1ccc` | Body text |
|
||||
| `colors.primary.text[60]` | `#1c1c1c99` | Eyebrows, meta, captions |
|
||||
| `colors.primary.text[40]` | `#1c1c1c66` | Disabled / placeholder |
|
||||
| `colors.primary.text[20]` | `#1c1c1c33` | Subtle separators |
|
||||
| `colors.primary.text[10]` | `#1c1c1c1a` | Hairline borders |
|
||||
| `colors.primary.text[5]` | `#1c1c1c0d` | Subtle fills (rates panel, skill chips) |
|
||||
| `colors.primary.border[10]` | `#1c1c1c1a` | Default border |
|
||||
| `colors.primary.border[20]` | `#1c1c1c33` | Hover border |
|
||||
|
||||
Reverse palette (for dark CTAs):
|
||||
|
||||
| Token | Role |
|
||||
| --- | --- |
|
||||
| `colors.secondary.background[100]` | Filled CTA background (deep ink) |
|
||||
| `colors.secondary.text[100]` | Filled CTA text (white) |
|
||||
|
||||
Brand accents (currently absent from partner pages; available if needed):
|
||||
|
||||
- `colors.accent.blue` — `#4a38f5` / `#8174f8`
|
||||
- `colors.accent.pink` — `#ed87fc` / `#f3abfd`
|
||||
- `colors.accent.yellow` — `#feffb7` / `#feffd9`
|
||||
- `colors.accent.green` — `#89fc9a` / `#b0fdbe`
|
||||
- `colors.highlight` — same hue as blue accent
|
||||
|
||||
**Do not introduce gradients, glass blurs, or saturated fills on partner pages.** Color is conviction here, not decoration.
|
||||
|
||||
## Typography
|
||||
|
||||
Three families, each load-balanced via CSS variables:
|
||||
|
||||
| Family | Var | Use |
|
||||
| --- | --- | --- |
|
||||
| `theme.font.family.serif` | `--font-serif` | Headlines, partner names, headline values |
|
||||
| `theme.font.family.sans` | `--font-sans` | Body, prose, interactive labels |
|
||||
| `theme.font.family.mono` | `--font-mono` | Eyebrows, meta, currency labels, tabular numerics |
|
||||
| `theme.font.family.retro` | `--font-retro` | Reserved (not used on partner pages) |
|
||||
|
||||
### Weight + Size Contrast
|
||||
|
||||
Weights: `light: 300`, `regular: 400`, `medium: 500`. No bold. Hierarchy is driven by scale and family contrast, never by weight alone.
|
||||
|
||||
Scale (`theme.font.size(n)` → `calc(var(--font-base) * n)`, where `--font-base: 0.25rem` ≈ 4px):
|
||||
|
||||
- Display / h1: size 9–12 (36–48px)
|
||||
- h2 / section heads: size 7–8 (28–32px)
|
||||
- h3 / card heads: size 5–6 (20–24px)
|
||||
- Body / prose: size 4–5 (16–20px)
|
||||
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.06–0.08em` and `text-transform: uppercase`
|
||||
|
||||
Body line length: cap at 65–75ch (the existing `PartnerProfileIntro` uses `max-width: 62ch` — keep that order of magnitude).
|
||||
|
||||
### Hierarchy contract
|
||||
|
||||
- A serif `<h1>` at size 9 light reads as a partner's name on the detail page.
|
||||
- A mono eyebrow above or below it locates the partner (region · city · country).
|
||||
- A serif size 6 light reads as a section head.
|
||||
- Body prose is sans regular.
|
||||
- Currency values are serif (they read as headline numbers, not stats).
|
||||
- Currency labels and meta are mono.
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
Base unit `4px`. Spacing helper `theme.spacing(n)` returns `n * 4px`. Common rhythms on the partner pages:
|
||||
|
||||
- Inter-section gap on the detail page: `theme.spacing(10–14)` — generous, editorial breathing room.
|
||||
- Inter-element gap inside a section: `theme.spacing(3–5)`.
|
||||
- Card padding: `theme.spacing(6)`.
|
||||
- Page horizontal padding: `theme.spacing(4)` mobile, `theme.spacing(10)` ≥ md breakpoint.
|
||||
|
||||
### Radius
|
||||
|
||||
`theme.radius(n)` returns `n * 2px`. The default card radius is `theme.radius(2)` = 4px. Pills use `999px`. No softer rounding than that.
|
||||
|
||||
### Borders
|
||||
|
||||
Borders are hairline (`1px solid theme.colors.primary.border[10]`). They define edges quietly. On hover they step to `border[20]`. Never use a chunky border as decoration.
|
||||
|
||||
## Components
|
||||
|
||||
### Card (PartnerCard, RatesPanel)
|
||||
|
||||
White surface, hairline border, 4px radius, 24px padding, soft shadow on hover only:
|
||||
|
||||
```css
|
||||
background-color: ${theme.colors.primary.background[100]};
|
||||
border: 1px solid ${theme.colors.primary.border[10]};
|
||||
border-radius: ${theme.radius(2)};
|
||||
padding: ${theme.spacing(6)};
|
||||
|
||||
&:hover {
|
||||
border-color: ${theme.colors.primary.border[20]};
|
||||
box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.18);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
### Chip / Pill
|
||||
|
||||
Rounded `999px`, 1px border, subtle background fill (`primary.text[5]` for filter pills, transparent for chip rows), `text[80]` color, mono or sans font.
|
||||
|
||||
### Button / LinkButton
|
||||
|
||||
Lives in `@/design-system/components`. Two color modes: `primary` (deep ink fill, white text) and `secondary` (transparent fill, ink text + 1px border). `variant="contained"` is what partner pages use.
|
||||
|
||||
### Avatar
|
||||
|
||||
`PartnerAvatar` is a deterministic generated mark from name + slug. Used as fallback when `profilePictureUrl` is missing. The real photo overlays it at 120px circle on the detail page, 56px on the list card.
|
||||
|
||||
## Motion
|
||||
|
||||
- Hover transitions: 250ms, ease-out (cubic-bezier curve in `PartnerCard`: `0.25s ease`).
|
||||
- Card entrance: 700ms cubic-bezier `0.22, 1, 0.36, 1` (ease-out-quart), 90ms stagger per index.
|
||||
- All motion respects `@media (prefers-reduced-motion: reduce)` — animations stop, hover translate disabled.
|
||||
- **No bounce, no elastic, no parallax.** Editorial restraint.
|
||||
|
||||
## Iconography
|
||||
|
||||
`@tabler/icons-react`, 14–16px on body-level chips, 18–24px on buttons. Always `aria-hidden="true"` when decorative. Stroke width `2` (default).
|
||||
|
||||
## Accessibility Defaults
|
||||
|
||||
- Focus ring: `outline: 2px solid theme.colors.primary.text[100]; outline-offset: 4px` (already used on the card link).
|
||||
- Touch target ≥ 40×40px on mobile.
|
||||
- `aria-label` on icon-only buttons, `aria-labelledby` on sectioned regions.
|
||||
- All `<a target="_blank">` includes `rel="noopener noreferrer"`.
|
||||
- Color is never the sole carrier of meaning. The money pills carry both an icon and a text label.
|
||||
|
||||
## Anti-patterns (project-specific)
|
||||
|
||||
In addition to the impeccable shared absolute bans:
|
||||
|
||||
- **Do not use the brand accent colors (blue/pink/yellow/green) on partner pages** unless we have a stronger reason than "to add color".
|
||||
- **No skeuomorphic shadows on cards.** The hover shadow is `0 12px 32px -16px rgba(0,0,0,0.18)` — that's the ceiling.
|
||||
- **No gradients on anything.** Including text, borders, and backgrounds.
|
||||
- **No floating "Trusted by" logo bars** on partner pages.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Twenty Website — Product & Brand Context
|
||||
|
||||
> Strategic context for design work on the Twenty marketing site (`packages/twenty-website`). Loaded by every `impeccable` invocation.
|
||||
|
||||
## Register
|
||||
|
||||
**Brand.** The marketing site is a public-facing surface where the design itself is part of the credibility argument. Prospects evaluate Twenty partly by how the site feels. The product app (`packages/twenty-front`) is a separate product-register surface, governed elsewhere.
|
||||
|
||||
## Users & Purpose
|
||||
|
||||
The primary audience varies by route, but the working assumption for partner-related pages is:
|
||||
|
||||
- **Who:** A budget-holding decision maker (founder, RevOps lead, or COO) shopping for a CRM implementation partner. Already on Twenty's site, evaluating a shortlist of partners.
|
||||
- **Context:** Doing a side-by-side comparison across 2–5 candidates over a single browsing session. Will spend 30–90 seconds on each profile before deciding whether to book a call.
|
||||
- **Decision being made:** "Is this partner credible, the right size, the right specialty, and within budget? Do I trust them enough to commit 30 minutes to a discovery call?"
|
||||
|
||||
What the partner pages must do, in priority order:
|
||||
1. Communicate credibility (real firm, real person, real work).
|
||||
2. Surface fit signals fast (skills, region, languages, deployment expertise, budget range).
|
||||
3. Give the visitor a confident "next step" affordance (book a call or vet via LinkedIn) without pressure.
|
||||
|
||||
## Desired Outcome
|
||||
|
||||
The redesign should make `/partners/profile/[slug]` feel like a *thoughtfully curated profile of a top-tier partner*, not a generic templated card. A visitor should leave thinking "this firm is serious" even if they don't book a call this session.
|
||||
|
||||
Specifically:
|
||||
- **Confidence over information density.** A short, well-typeset profile beats a packed-but-busy one.
|
||||
- **Editorial restraint.** White space, deliberate type hierarchy, and a few well-chosen details say more than dozens of small components.
|
||||
- **Quiet conviction.** No hype copy, no growth-hack patterns, no "Trusted by" logo strips. The partner's own work and intro speak for themselves.
|
||||
|
||||
## Brand Personality
|
||||
|
||||
**Editorial · Founder-led · Considered.**
|
||||
|
||||
The site reads like a thoughtful indie publication, not a SaaS landing page. Serif headlines, plenty of whitespace, deliberate typographic rhythm. Quietly opinionated — Twenty has a point of view about CRM (open-source, customizable, well-designed) and the site reflects that without shouting.
|
||||
|
||||
Tonal anchors:
|
||||
- Stripe's documentation for clarity, Linear's marketing for restraint, an editorial print magazine for typography choices.
|
||||
|
||||
## Anti-references
|
||||
|
||||
**Reject these patterns. They make the work read as generic AI / generic SaaS:**
|
||||
|
||||
- **Generic SaaS landing.** Big-number heroes, identical icon-grid cards, gradient text, navy + lime accent color schemes, "supercharge your workflow" language.
|
||||
- **Corporate enterprise tone.** Stock photos of diverse handshakes. "Trusted by Fortune 500" logo strips as the primary credibility move. Trust-badge bars.
|
||||
- **Bento templates.** Repetitive same-size cards. Vercel-style scroll-pin animations on every section.
|
||||
- **Side-stripe borders, gradient text, glassmorphism, hero-metric templates, identical card grids** — see impeccable's shared absolute bans.
|
||||
|
||||
## Strategic Design Principles
|
||||
|
||||
1. **Typography carries the design.** The brand has a serif/sans/mono trio. Hierarchy is set by scale + weight contrast, not by color or borders.
|
||||
2. **Restrained palette.** Tinted neutrals (black/white via CSS variables, with alpha-tone variants for text and borders) carry 90%+ of the surface. Accent color used sparingly when it appears at all.
|
||||
3. **Whitespace is a feature.** Tight cards feel cheap. Pages should breathe.
|
||||
4. **Asymmetry over grid.** A 12-col bento is the wrong shape for a profile page. Use asymmetric two-column layouts where one column does heavy lifting.
|
||||
5. **One opinionated detail per page.** Each surface should have one moment of editorial conviction (a typographic flourish, a precise micro-interaction, a deliberate space) rather than five generic flourishes.
|
||||
|
||||
## Accessibility
|
||||
|
||||
**WCAG AA + keyboard + screen reader baseline:**
|
||||
|
||||
- All interactive elements reachable by keyboard, focus visible (`outline: 2px solid`, not just color shift).
|
||||
- Semantic landmarks: `<header>`, `<main>`, `<nav>`, `<section aria-labelledby=…>`, headings in order.
|
||||
- All images with informational content have alt text. Decorative icons have `aria-hidden="true"`.
|
||||
- Body text ≥ 4.5:1 contrast; large text (≥18pt or 14pt bold) ≥ 3:1.
|
||||
- Respect `prefers-reduced-motion`. Animations stop, don't slow.
|
||||
- Forms have explicit labels. Errors are announced.
|
||||
|
||||
## Tech & Constraints
|
||||
|
||||
- Next.js 16 app router (Server Components by default, `'use client'` for interactivity).
|
||||
- Linaria styled-components (`@linaria/react`) for zero-runtime CSS-in-JS.
|
||||
- Lingui (`@lingui/react`) for i18n; never hardcode user-visible strings.
|
||||
- Theme tokens in `packages/twenty-website/src/theme/`. Colors are CSS variables resolved to OKLCH-tinted neutrals.
|
||||
- `@tabler/icons-react` for iconography (no Heroicons, no custom SVGs unless purposeful).
|
||||
- `@radix-ui/react-*` for primitives (Popover etc) where headless behavior is needed.
|
||||
|
||||
## Out of Scope for This File
|
||||
|
||||
- Detailed visual tokens (colors, type scale, motion specs) live in `DESIGN.md`.
|
||||
- Per-page IA decisions live in shape briefs (`docs/superpowers/specs/`).
|
||||
@@ -0,0 +1,164 @@
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center">The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/github-cover-light.webp" alt="Twenty banner" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
# Why Twenty
|
||||
|
||||
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
|
||||
|
||||
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
|
||||
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
|
||||
Scaffold a new app with the Twenty CLI:
|
||||
|
||||
```bash
|
||||
npx create-twenty-app my-app
|
||||
```
|
||||
|
||||
Define objects, fields, and views as code:
|
||||
|
||||
```ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineObject({
|
||||
nameSingular: 'deal',
|
||||
namePlural: 'deals',
|
||||
labelSingular: 'Deal',
|
||||
labelPlural: 'Deals',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Name', type: FieldType.TEXT },
|
||||
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
|
||||
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Then ship it to your workspace:
|
||||
|
||||
```bash
|
||||
npx twenty app:publish --private
|
||||
```
|
||||
|
||||
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
|
||||
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
# Everything you need
|
||||
|
||||
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
|
||||
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
# Stack
|
||||
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
|
||||
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
|
||||
|
||||
# Thanks
|
||||
|
||||
<p align="center">
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
|
||||
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
|
||||
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
|
||||
# Join the Community
|
||||
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
@@ -0,0 +1,139 @@
|
||||
name: twenty
|
||||
|
||||
services:
|
||||
server:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
NODE_PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REACT_APP_SERVER_BASE_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS}
|
||||
DISABLE_CRON_JOBS_REGISTRATION: ${DISABLE_CRON_JOBS_REGISTRATION}
|
||||
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
FALLBACK_ENCRYPTION_KEY: ${FALLBACK_ENCRYPTION_KEY}
|
||||
APP_SECRET: ${APP_SECRET:-}
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
# AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET}
|
||||
# AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL}
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL}
|
||||
|
||||
# CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED}
|
||||
# MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID}
|
||||
# AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET}
|
||||
# AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL}
|
||||
# AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL}
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:3000/healthz
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
DISABLE_CRON_JOBS_REGISTRATION: "true" # it already runs on the server
|
||||
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
|
||||
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
FALLBACK_ENCRYPTION_KEY: ${FALLBACK_ENCRYPTION_KEY}
|
||||
APP_SECRET: ${APP_SECRET:-}
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED}
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED}
|
||||
# AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID}
|
||||
# AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET}
|
||||
# AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL}
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL}
|
||||
|
||||
# CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED}
|
||||
# MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED}
|
||||
# AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID}
|
||||
# AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET}
|
||||
# AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL}
|
||||
# AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL}
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
server:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${PG_DATABASE_NAME:-default}
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
POSTGRES_USER: ${PG_DATABASE_USER:-postgres}
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
server-local-data:
|
||||
@@ -0,0 +1,284 @@
|
||||
{
|
||||
"workspaceLayout": {
|
||||
"appsDir": "packages",
|
||||
"libsDir": "packages"
|
||||
},
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*"],
|
||||
"excludeStories": [
|
||||
"default",
|
||||
"!{projectRoot}/.storybook/*",
|
||||
"!{projectRoot}/**/tsconfig.storybook.json",
|
||||
"!{projectRoot}/**/*.stories.(ts|tsx)",
|
||||
"!{projectRoot}/**/__stories__/*"
|
||||
],
|
||||
"excludeTests": [
|
||||
"default",
|
||||
"!{projectRoot}/**/jest.config.(js|ts)",
|
||||
"!{projectRoot}/**/tsconfig.spec.json",
|
||||
"!{projectRoot}/**/*.test.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.spec.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.integration-spec.ts",
|
||||
"!{projectRoot}/**/__tests__/*"
|
||||
],
|
||||
"production": [
|
||||
"default",
|
||||
"excludeStories",
|
||||
"excludeTests",
|
||||
"!{projectRoot}/**/__mocks__/*",
|
||||
"!{projectRoot}/**/testing/*"
|
||||
]
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"cache": true,
|
||||
"inputs": ["^production", "production"],
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"start": {
|
||||
"cache": false,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
|
||||
},
|
||||
"lint:diff-with-main": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"dependsOn": ["twenty-oxlint-rules:build"],
|
||||
"options": {
|
||||
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
|
||||
"pattern": "\\.(ts|tsx|js|jsx)$"
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {
|
||||
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx oxfmt --check {args.files} {args.write}",
|
||||
"files": ".",
|
||||
"write": ""
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"command": "npx oxfmt {args.files}"
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "tsgo -p tsconfig.json"
|
||||
},
|
||||
"configurations": {
|
||||
"watch": {
|
||||
"command": "tsgo -p tsconfig.json --watch"
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeStories",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
],
|
||||
"outputs": ["{projectRoot}/coverage"],
|
||||
"options": {
|
||||
"jestConfig": "{projectRoot}/jest.config.mjs",
|
||||
"silent": true,
|
||||
"coverage": true,
|
||||
"coverageReporters": ["text-summary"],
|
||||
"cacheDirectory": "../../.cache/jest/{projectRoot}"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"maxWorkers": 1
|
||||
},
|
||||
"coverage": {
|
||||
"coverageReporters": ["lcov", "text"]
|
||||
},
|
||||
"watch": {
|
||||
"watch": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"test:e2e": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"set-local-version": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "node -e \"const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='{args.releaseVersion}';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n');\""
|
||||
}
|
||||
},
|
||||
"storybook:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "NODE_OPTIONS='--max-old-space-size=10240' storybook build --test",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "storybook dev",
|
||||
"config-dir": ".storybook"
|
||||
}
|
||||
},
|
||||
"storybook:serve:static": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["storybook:build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx http-server {args.staticDir} -a={args.host} --port={args.port} --silent={args.silent}",
|
||||
"staticDir": "storybook-static",
|
||||
"host": "localhost",
|
||||
"port": 6006,
|
||||
"silent": true
|
||||
}
|
||||
},
|
||||
"storybook:test": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"outputs": ["{projectRoot}/coverage/storybook"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "vitest run --coverage --shard={args.shard}",
|
||||
"shard": "1/1"
|
||||
}
|
||||
},
|
||||
"storybook:test:no-coverage": {
|
||||
"executor": "nx:run-commands",
|
||||
"inputs": ["^default", "excludeTests"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "vitest run --shard={args.shard}",
|
||||
"shard": "1/1"
|
||||
}
|
||||
},
|
||||
"storybook:coverage": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeTests",
|
||||
"{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/coverage/storybook",
|
||||
"!{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||
],
|
||||
"options": {
|
||||
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage={args.checkCoverage} --cwd={projectRoot}",
|
||||
"coverageDir": "coverage/storybook",
|
||||
"reporter": "lcov",
|
||||
"checkCoverage": true
|
||||
},
|
||||
"configurations": {
|
||||
"text": {
|
||||
"reporter": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/jest:jest": {
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"^default",
|
||||
"excludeStories",
|
||||
"{workspaceRoot}/jest.preset.js"
|
||||
],
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nx/vite:build": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["default", "^default"]
|
||||
},
|
||||
"@nx/vitest:test": {
|
||||
"cache": true,
|
||||
"inputs": ["default", "^default"]
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
"style": "@linaria/react",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
"unitTestRunner": "jest",
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"library": {
|
||||
"style": "@linaria/react",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
"unitTestRunner": "jest",
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"component": {
|
||||
"style": "@linaria/react"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"options": {
|
||||
"cacheableOperations": ["storybook:build"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"useInferencePlugins": false,
|
||||
"defaultBase": "main",
|
||||
"tui": {
|
||||
"enabled": false
|
||||
},
|
||||
"analytics": false
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:latest
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
- "3001:3001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 128M
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Vaultwarden Changelog
|
||||
|
||||
## 2026-07-05
|
||||
|
||||
### Initial deployment
|
||||
- Deployed vaultwarden/server:1.33.2 on app1 (netcup)
|
||||
- Port 8080 mapped to container port 80
|
||||
- Admin panel enabled via ADMIN_TOKEN in .env
|
||||
- Signups disabled, invitations allowed
|
||||
- Volume: ./data:/data
|
||||
- Pending: Caddy reverse proxy + Let's Encrypt for vaultwarden.app1.itpropartner.com
|
||||
- Pending: Add S3 backup of ./data/ to daily backup script
|
||||
- Compose files synced to S3 every 15 min via hermes-live-sync (data excluded for now)
|
||||
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:1.33.2
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
environment:
|
||||
- DOMAIN=https://vaultwarden.tailc2f3b0.ts.net
|
||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
||||
- SIGNUPS_ALLOWED=false
|
||||
- SIGNUPS_VERIFY=false
|
||||
- INVITATIONS_ALLOWED=true
|
||||
env_file:
|
||||
- .env
|
||||
Reference in New Issue
Block a user