Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e58f6ab6d9 | ||
|
|
c3b75d0ad2 | ||
|
|
e456642904 | ||
|
|
f855fa314d | ||
|
|
85b65d12d2 | ||
|
|
9dde87c20f | ||
|
|
cdaebdb16c | ||
|
|
3f3e42b743 | ||
|
|
22f3fd7a7a | ||
|
|
37e6353c1d | ||
|
|
d2eb3474c6 | ||
|
|
3587e195ac | ||
|
|
d54ea4fd11 | ||
|
|
2cd2f7c29c | ||
|
|
7535b90ec6 | ||
|
|
1fb51bb510 | ||
|
|
1bb257a811 | ||
|
|
aac099f2e8 | ||
|
|
a3c0c62506 | ||
|
|
390ae34ed8 | ||
|
|
fe59c333ac | ||
|
|
6c0da189cb | ||
|
|
5aea16f070 | ||
|
|
2f542d6763 | ||
|
|
4ac38449cb | ||
|
|
dab4cbcb7d | ||
|
|
a73f586619 | ||
|
|
d1126f507d | ||
|
|
3a1f10e572 | ||
|
|
d16299a624 | ||
|
|
61d058bb1f |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
**/node_modules
|
||||
node_modules
|
||||
.nx/cache
|
||||
packages/twenty-server/.env
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
name: AI Catalog Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 6 AM UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-catalog:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Build twenty-server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Run catalog sync
|
||||
run: npx nx run twenty-server:command-no-deps ai:sync-models-dev
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet packages/twenty-server/src/engine/metadata-modules/ai/ai-models/ai-providers.json; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AI model catalog from models.dev'
|
||||
title: 'chore: sync AI model catalog from models.dev'
|
||||
body: |
|
||||
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev).
|
||||
|
||||
This PR updates pricing, context windows, and model availability based on the latest data.
|
||||
New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically.
|
||||
Deprecated models are detected based on cost-efficiency within the same model family.
|
||||
|
||||
**Please review before merging** — verify no critical models were incorrectly deprecated.
|
||||
branch: chore/ai-catalog-sync
|
||||
base: main
|
||||
labels: ai, automated
|
||||
delete-branch: true
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app" --skip-local-instance
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app"
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
@@ -151,24 +151,22 @@ jobs:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
|
||||
npx --no-install twenty auth:login --api-key $SEED_API_KEY --api-url http://localhost:3000
|
||||
|
||||
- name: Build scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty build
|
||||
npx --no-install twenty app:build
|
||||
test -d .twenty/output
|
||||
|
||||
- name: Execute hello-world logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
|
||||
EXEC_OUTPUT=$(npx --no-install twenty function:execute --functionName hello-world-logic-function)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -10,8 +10,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
|
||||
@@ -40,8 +40,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
@@ -112,7 +111,7 @@ jobs:
|
||||
run: yarn docs:generate-paths
|
||||
|
||||
- name: Commit artifacts to pull request branch
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json packages/twenty-shared/src/constants/DocumentationPaths.ts
|
||||
if git diff --staged --quiet --exit-code; then
|
||||
@@ -150,3 +149,4 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
echo "Seeding full dev workspace..."
|
||||
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
|
||||
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev --with-apps; then
|
||||
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
|
||||
docker compose logs server
|
||||
exit 1
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres", "${PG_DATABASE_URL}"],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
|
||||
@@ -80,17 +80,6 @@ npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/mi
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
```
|
||||
|
||||
### 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 or `workspace:sync-metadata` 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)
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-host/capabilities/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/contribute/capabilities/local-setup)
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
|
||||
# Why Twenty
|
||||
|
||||
@@ -36,7 +36,7 @@ We built Twenty for three reasons:
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
|
||||
**We believe in open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.0.0",
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
|
||||
@@ -25,47 +25,41 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# The scaffolder can automatically:
|
||||
# 1. Start a local Twenty server (Docker)
|
||||
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
|
||||
# 3. Authenticate your app via OAuth
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
|
||||
# Or do it manually:
|
||||
yarn twenty server start # Start local Twenty server
|
||||
yarn twenty remote add --local # Authenticate via OAuth
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
|
||||
yarn twenty app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn twenty exec -n my-function -p '{"key": "value"}'
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Execute the pre-install function
|
||||
yarn twenty exec --preInstall
|
||||
yarn twenty function:execute --preInstall
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty build
|
||||
|
||||
# Publish the app to npm or directly to a Twenty server
|
||||
yarn twenty publish
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty uninstall
|
||||
yarn twenty app:uninstall
|
||||
```
|
||||
|
||||
## Scaffolding modes
|
||||
@@ -107,69 +101,43 @@ npx create-twenty-app@latest my-app -m
|
||||
- `skills/example-skill.ts` — Example AI agent skill definition
|
||||
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
|
||||
|
||||
## Local server
|
||||
|
||||
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
|
||||
|
||||
```bash
|
||||
yarn twenty server start # Start (pulls image if needed)
|
||||
yarn twenty server status # Check if it's healthy
|
||||
yarn twenty server logs # Stream logs
|
||||
yarn twenty server stop # Stop (data is preserved)
|
||||
yarn twenty server reset # Wipe all data and start fresh
|
||||
```
|
||||
|
||||
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
|
||||
|
||||
## Next steps
|
||||
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
|
||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
|
||||
## Build and publish your application
|
||||
## Publish your application
|
||||
|
||||
Once your app is ready, build and publish it using the CLI:
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
|
||||
```bash
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty build --tarball
|
||||
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty publish --tag beta
|
||||
|
||||
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
|
||||
yarn twenty deploy
|
||||
```
|
||||
|
||||
### Publish to the Twenty marketplace
|
||||
|
||||
You can also contribute your application to the curated marketplace:
|
||||
You can share your application with all Twenty users:
|
||||
|
||||
```bash
|
||||
# pull the Twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- Copy your app folder into `twenty/packages/twenty-apps`.
|
||||
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
||||
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
|
||||
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.1",
|
||||
"version": "0.7.0-canary.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -27,10 +27,6 @@ const program = new Command(packageJson.name)
|
||||
'--description <description>',
|
||||
'Application description (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -41,7 +37,6 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
@@ -77,7 +72,6 @@ const program = new Command(packageJson.name)
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,4 +11,3 @@
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
|
||||
@@ -1,11 +1,62 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
|
||||
|
||||
```bash
|
||||
# Make sure a Twenty server is running at http://localhost:3000
|
||||
yarn test
|
||||
```
|
||||
|
||||
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import {
|
||||
type LocalInstanceResult,
|
||||
setupLocalInstance,
|
||||
} from '@/utils/setup-local-instance';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as path from 'path';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -27,7 +22,6 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
@@ -46,7 +40,6 @@ export class CreateAppCommand {
|
||||
|
||||
await fs.ensureDir(appDirectory);
|
||||
|
||||
console.log(chalk.gray(' Scaffolding project files...'));
|
||||
await copyBaseApplicationProject({
|
||||
appName,
|
||||
appDisplayName,
|
||||
@@ -55,24 +48,11 @@ export class CreateAppCommand {
|
||||
exampleOptions,
|
||||
});
|
||||
|
||||
console.log(chalk.gray(' Installing dependencies...'));
|
||||
await install(appDirectory);
|
||||
|
||||
console.log(chalk.gray(' Initializing git repository...'));
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
let localResult: LocalInstanceResult = { running: false };
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
// Auto-detect a running server first
|
||||
localResult = await setupLocalInstance(appDirectory);
|
||||
|
||||
if (localResult.running && localResult.serverUrl) {
|
||||
await this.connectToLocal(appDirectory, localResult.serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, localResult);
|
||||
this.logSuccess(appDirectory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
@@ -193,55 +173,22 @@ export class CreateAppCommand {
|
||||
appDirectory: string;
|
||||
appName: string;
|
||||
}): void {
|
||||
console.log(chalk.blue('Creating Twenty Application'));
|
||||
console.log(chalk.gray(` Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(` Name: ${appName}`));
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private async connectToLocal(
|
||||
appDirectory: string,
|
||||
serverUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
execSync(
|
||||
`npx nx run twenty-sdk:start -- remote add ${serverUrl} --as local`,
|
||||
{
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
console.log(chalk.green('Authenticated with local Twenty instance.'));
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Authentication skipped. Run `npx nx run twenty-sdk:start -- remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
localResult: LocalInstanceResult,
|
||||
): void {
|
||||
private logSuccess(appDirectory: string): void {
|
||||
const dirName = appDirectory.split('/').reverse()[0] ?? '';
|
||||
|
||||
console.log(chalk.green('Application created!'));
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
|
||||
if (!localResult.running) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.gray(' yarn twenty dev # Start dev mode'),
|
||||
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ describe('scaffoldIntegrationTest', () => {
|
||||
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
expect(content).not.toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('setup-test.ts');
|
||||
expect(content).toContain('tsconfig.spec.json');
|
||||
expect(content).toContain('integration-test.ts');
|
||||
|
||||
@@ -474,7 +474,7 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -482,7 +482,6 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
@@ -610,9 +609,8 @@ const createPackageJson = async ({
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^19.0.0',
|
||||
react: '^19.0.0',
|
||||
'react-dom': '^19.0.0',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
|
||||
const DEFAULT_PORT = 2020;
|
||||
|
||||
// Minimal health check — the full implementation lives in twenty-sdk
|
||||
const isServerReady = async (port: number): Promise<boolean> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/healthz`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
export type LocalInstanceResult = {
|
||||
running: boolean;
|
||||
serverUrl?: string;
|
||||
};
|
||||
|
||||
export const setupLocalInstance = async (
|
||||
appDirectory: string,
|
||||
): Promise<LocalInstanceResult> => {
|
||||
console.log('');
|
||||
console.log(chalk.blue('Setting up local Twenty instance...'));
|
||||
|
||||
if (await isServerReady(DEFAULT_PORT)) {
|
||||
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
|
||||
|
||||
console.log(chalk.green(`Twenty server detected on ${serverUrl}.`));
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
// Delegate to `twenty server start` from the scaffolded app
|
||||
console.log(chalk.gray('Starting local Twenty server...'));
|
||||
|
||||
try {
|
||||
execSync('yarn twenty server start', {
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Failed to start Twenty server. Run `yarn twenty server start` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Waiting for Twenty to be ready...'));
|
||||
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = 180 * 1000;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (await isServerReady(DEFAULT_PORT)) {
|
||||
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
|
||||
|
||||
console.log(chalk.green(`Twenty server is running on ${serverUrl}.`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'Workspace ready — login with tim@apple.dev / tim@apple.dev',
|
||||
),
|
||||
);
|
||||
|
||||
const openCommand = platform() === 'darwin' ? 'open' : 'xdg-open';
|
||||
|
||||
try {
|
||||
execSync(`${openCommand} ${serverUrl}`, { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Ignore if browser can't be opened
|
||||
}
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Twenty server did not become healthy in time. Check: yarn twenty server logs',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
};
|
||||
@@ -45,6 +45,7 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'${SEED_API_KEY}',
|
||||
},
|
||||
@@ -93,7 +94,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
@@ -119,13 +120,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
generated
|
||||
.twenty
|
||||
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextData = {
|
||||
type RichTextV2Data = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -123,7 +123,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextData,
|
||||
} satisfies RichTextV2Data,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -159,7 +159,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextData;
|
||||
bodyV2: RichTextV2Data;
|
||||
dueAt?: string;
|
||||
} = {
|
||||
title: actionItem.title,
|
||||
|
||||
@@ -10,4 +10,3 @@
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextData = {
|
||||
type RichTextV2Data = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -362,7 +362,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextData,
|
||||
} satisfies RichTextV2Data,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -451,7 +451,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextData;
|
||||
bodyV2: RichTextV2Data;
|
||||
dueAt?: string;
|
||||
assigneeId?: string;
|
||||
} = {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"dev": "twenty dev",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"app:dev": "twenty app:dev",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
generated
|
||||
node_modules
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-role';
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'6563e091-9f5b-4026-a3ea-7e3b3d09e218';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Hello world',
|
||||
description: '',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"application": {
|
||||
"universalIdentifier": "6563e091-9f5b-4026-a3ea-7e3b3d09e218",
|
||||
"displayName": "Hello world",
|
||||
"description": "",
|
||||
"defaultRoleUniversalIdentifier": "9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f",
|
||||
"yarnLockChecksum": null,
|
||||
"packageJsonChecksum": null,
|
||||
"apiClientChecksum": "6db455b2541abbd2d4e9f85469bdc532",
|
||||
"preInstallLogicFunctionUniversalIdentifier": "1272ffdb-8e2f-492c-ab37-66c2b97e9c23",
|
||||
"postInstallLogicFunctionUniversalIdentifier": "7a3f4684-51db-494d-833b-a747a3b90507"
|
||||
},
|
||||
"objects": [
|
||||
{
|
||||
"universalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"nameSingular": "exampleItem",
|
||||
"namePlural": "exampleItems",
|
||||
"labelSingular": "Example item",
|
||||
"labelPlural": "Example items",
|
||||
"description": "A sample custom object",
|
||||
"icon": "IconBox",
|
||||
"labelIdentifierFieldMetadataUniversalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
|
||||
"fields": [
|
||||
{
|
||||
"universalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
|
||||
"type": "TEXT",
|
||||
"name": "name",
|
||||
"label": "Name",
|
||||
"description": "Name of the example item",
|
||||
"icon": "IconAbc"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"description": "Id",
|
||||
"icon": "Icon123",
|
||||
"isNullable": false,
|
||||
"defaultValue": "uuid",
|
||||
"type": "UUID",
|
||||
"universalIdentifier": "5ba62991-6034-555c-b984-5e84897eaa9f"
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"label": "Creation date",
|
||||
"description": "Creation date",
|
||||
"icon": "IconCalendar",
|
||||
"isNullable": false,
|
||||
"defaultValue": "now",
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "8b616045-237c-574d-8689-73f06acff8d8"
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"label": "Last update",
|
||||
"description": "Last time the record was changed",
|
||||
"icon": "IconCalendarClock",
|
||||
"isNullable": false,
|
||||
"defaultValue": "now",
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "ced5587d-d9de-51bf-9ad0-74af6198fca4"
|
||||
},
|
||||
{
|
||||
"name": "deletedAt",
|
||||
"label": "Deleted at",
|
||||
"description": "Deletion date",
|
||||
"icon": "IconCalendarClock",
|
||||
"isNullable": true,
|
||||
"defaultValue": null,
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "674ee694-0b0f-557f-993c-2b1404db0811"
|
||||
},
|
||||
{
|
||||
"name": "createdBy",
|
||||
"label": "Created by",
|
||||
"description": "The creator of the record",
|
||||
"icon": "IconCreativeCommonsSa",
|
||||
"isNullable": false,
|
||||
"defaultValue": {
|
||||
"name": "''",
|
||||
"source": "'MANUAL'"
|
||||
},
|
||||
"type": "ACTOR",
|
||||
"universalIdentifier": "5ed1503f-2803-59d1-9b27-e4f1701b8c2a"
|
||||
},
|
||||
{
|
||||
"name": "updatedBy",
|
||||
"label": "Updated by",
|
||||
"description": "The workspace member who last updated the record",
|
||||
"icon": "IconUserCircle",
|
||||
"isNullable": false,
|
||||
"defaultValue": {
|
||||
"name": "''",
|
||||
"source": "'MANUAL'"
|
||||
},
|
||||
"type": "ACTOR",
|
||||
"universalIdentifier": "3d3a80b4-14a0-55a0-b534-1b23f043c1d5"
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"label": "Position",
|
||||
"description": "Position",
|
||||
"icon": "IconHierarchy2",
|
||||
"isNullable": false,
|
||||
"defaultValue": 0,
|
||||
"type": "POSITION",
|
||||
"universalIdentifier": "a714b336-d765-5dff-b34b-fcc6ab037e3b"
|
||||
},
|
||||
{
|
||||
"name": "searchVector",
|
||||
"label": "Search vector",
|
||||
"icon": "IconSearch",
|
||||
"description": "Search vector",
|
||||
"isNullable": true,
|
||||
"defaultValue": null,
|
||||
"type": "TS_VECTOR",
|
||||
"universalIdentifier": "068064e1-c9da-5772-9293-9e57183cee67"
|
||||
},
|
||||
{
|
||||
"name": "timelineActivities",
|
||||
"label": "Timeline Activities",
|
||||
"description": "Example items tied to the ExampleItem",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "e5feca94-536c-5779-8c33-05e39705bcd7",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "3d140ea1-fc57-5c8c-96fd-7d1525ed6a69",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5"
|
||||
},
|
||||
{
|
||||
"name": "favorites",
|
||||
"label": "Favorites",
|
||||
"description": "Example items tied to the ExampleItem",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "3781cc69-9d3f-58d0-927c-c1c552f37a1d",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "3043fec0-091c-59ff-95ef-b9d95fbfa3e6",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860"
|
||||
},
|
||||
{
|
||||
"name": "attachments",
|
||||
"label": "Attachments",
|
||||
"description": "Example items tied to the ExampleItem",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "3ad98da8-42e1-50e3-b73b-8c06fa720e2a",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "e1232464-8f83-5459-aa94-a40cda8d949a",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a"
|
||||
},
|
||||
{
|
||||
"name": "noteTargets",
|
||||
"label": "Note Targets",
|
||||
"description": "Example items tied to the ExampleItem",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "67cae4ab-7d3d-5a66-8d06-ca24568ebe76",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400"
|
||||
},
|
||||
{
|
||||
"name": "taskTargets",
|
||||
"label": "Task Targets",
|
||||
"description": "Example items tied to the ExampleItem",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "a3b9f10c-f780-58cc-bbc9-402ed5f136f6",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "5d46d303-452d-5c9c-ab64-0a9768b87956",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"objectUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"universalIdentifier": "770d32c2-cf12-4ab2-b66d-73f92dc239b5",
|
||||
"type": "NUMBER",
|
||||
"name": "priority",
|
||||
"label": "Priority",
|
||||
"description": "Priority level for the example item (1-10)"
|
||||
},
|
||||
{
|
||||
"name": "targetExampleItem",
|
||||
"label": "ExampleItem",
|
||||
"description": "ExampleItem Example item",
|
||||
"icon": "IconTimelineEvent",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetExampleItemId"
|
||||
},
|
||||
"universalIdentifier": "3d140ea1-fc57-5c8c-96fd-7d1525ed6a69",
|
||||
"objectUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "e5feca94-536c-5779-8c33-05e39705bcd7",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1"
|
||||
},
|
||||
{
|
||||
"name": "targetExampleItem",
|
||||
"label": "ExampleItem",
|
||||
"description": "ExampleItem Example item",
|
||||
"icon": "IconHeart",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetExampleItemId"
|
||||
},
|
||||
"universalIdentifier": "3043fec0-091c-59ff-95ef-b9d95fbfa3e6",
|
||||
"objectUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "3781cc69-9d3f-58d0-927c-c1c552f37a1d",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"type": "RELATION"
|
||||
},
|
||||
{
|
||||
"name": "targetExampleItem",
|
||||
"label": "ExampleItem",
|
||||
"description": "ExampleItem Example item",
|
||||
"icon": "IconFileImport",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetExampleItemId"
|
||||
},
|
||||
"universalIdentifier": "e1232464-8f83-5459-aa94-a40cda8d949a",
|
||||
"objectUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "3ad98da8-42e1-50e3-b73b-8c06fa720e2a",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f634-435d-ab8d-e1168b375c69"
|
||||
},
|
||||
{
|
||||
"name": "targetExampleItem",
|
||||
"label": "ExampleItem",
|
||||
"description": "ExampleItem Example item",
|
||||
"icon": "IconCheckbox",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetExampleItemId"
|
||||
},
|
||||
"universalIdentifier": "9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318",
|
||||
"objectUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "67cae4ab-7d3d-5a66-8d06-ca24568ebe76",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f635-435d-ab8d-e1168b375c70"
|
||||
},
|
||||
{
|
||||
"name": "targetExampleItem",
|
||||
"label": "ExampleItem",
|
||||
"description": "ExampleItem Example item",
|
||||
"icon": "IconCheckbox",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetExampleItemId"
|
||||
},
|
||||
"universalIdentifier": "5d46d303-452d-5c9c-ab64-0a9768b87956",
|
||||
"objectUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "a3b9f10c-f780-58cc-bbc9-402ed5f136f6",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f636-435d-ab8d-e1168b375c71"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"universalIdentifier": "9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f",
|
||||
"label": "Hello world default function role",
|
||||
"description": "Hello world default function role",
|
||||
"canReadAllObjectRecords": true,
|
||||
"canUpdateAllObjectRecords": true,
|
||||
"canSoftDeleteAllObjectRecords": true,
|
||||
"canDestroyAllObjectRecords": false
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"universalIdentifier": "d0940029-9d3c-40be-903a-52d65393028f",
|
||||
"name": "example-skill",
|
||||
"label": "Example Skill",
|
||||
"description": "A sample skill for your application",
|
||||
"icon": "IconBrain",
|
||||
"content": "Add your skill instructions here. Skills provide context and capabilities to AI agents."
|
||||
}
|
||||
],
|
||||
"agents": [],
|
||||
"logicFunctions": [
|
||||
{
|
||||
"universalIdentifier": "2baa26eb-9aaf-4856-a4f4-30d6fd6480ee",
|
||||
"name": "hello-world-logic-function",
|
||||
"description": "A simple logic function",
|
||||
"timeoutSeconds": 5,
|
||||
"httpRouteTriggerSettings": {
|
||||
"path": "/hello-world-logic-function",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": false
|
||||
},
|
||||
"toolInputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"handlerName": "default.config.handler",
|
||||
"sourceHandlerPath": "src/logic-functions/hello-world.ts",
|
||||
"builtHandlerPath": "src/logic-functions/hello-world.mjs",
|
||||
"builtHandlerChecksum": "f1a0565e18366e6c531438631137d23b"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "7a3f4684-51db-494d-833b-a747a3b90507",
|
||||
"name": "post-install",
|
||||
"description": "Runs after installation to set up the application.",
|
||||
"timeoutSeconds": 300,
|
||||
"toolInputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"handlerName": "default.config.handler",
|
||||
"sourceHandlerPath": "src/logic-functions/post-install.ts",
|
||||
"builtHandlerPath": "src/logic-functions/post-install.mjs",
|
||||
"builtHandlerChecksum": "7b016a1819c4e2355a80a895464032a5"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "1272ffdb-8e2f-492c-ab37-66c2b97e9c23",
|
||||
"name": "pre-install",
|
||||
"description": "Runs before installation to prepare the application.",
|
||||
"timeoutSeconds": 300,
|
||||
"toolInputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"handlerName": "default.config.handler",
|
||||
"sourceHandlerPath": "src/logic-functions/pre-install.ts",
|
||||
"builtHandlerPath": "src/logic-functions/pre-install.mjs",
|
||||
"builtHandlerChecksum": "e2151e31924929390a5933ebf7419465"
|
||||
}
|
||||
],
|
||||
"frontComponents": [
|
||||
{
|
||||
"universalIdentifier": "d371f098-5b2c-42f0-898d-94459f1ee337",
|
||||
"name": "hello-world-front-component",
|
||||
"description": "A sample front component",
|
||||
"componentName": "HelloWorld",
|
||||
"sourceComponentPath": "src/front-components/hello-world.tsx",
|
||||
"builtComponentPath": "src/front-components/hello-world.mjs",
|
||||
"builtComponentChecksum": "826d5d3f633496d7c2a4f902589b83ed",
|
||||
"isHeadless": false
|
||||
}
|
||||
],
|
||||
"publicAssets": [],
|
||||
"views": [
|
||||
{
|
||||
"universalIdentifier": "e004df40-29f3-47ba-b39d-d3a5c444367a",
|
||||
"name": "All example items",
|
||||
"objectUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
|
||||
"icon": "IconList",
|
||||
"key": "INDEX",
|
||||
"position": 0,
|
||||
"fields": [
|
||||
{
|
||||
"universalIdentifier": "496c40c2-5766-419c-93bf-20fdad3f34bb",
|
||||
"fieldMetadataUniversalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
|
||||
"position": 0,
|
||||
"isVisible": true,
|
||||
"size": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"navigationMenuItems": [
|
||||
{
|
||||
"universalIdentifier": "10f90627-e9c2-44b7-9742-bed77e3d1b17",
|
||||
"name": "example-navigation-menu-item",
|
||||
"icon": "IconList",
|
||||
"color": "blue",
|
||||
"position": 0,
|
||||
"viewUniversalIdentifier": "e004df40-29f3-47ba-b39d-d3a5c444367a"
|
||||
}
|
||||
],
|
||||
"pageLayouts": []
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "hello-world-app",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"app:dev": "twenty app:dev",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"react": "^19.0.2",
|
||||
"oxlint": "^0.16.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineField, FieldType } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '770d32c2-cf12-4ab2-b66d-73f92dc239b5',
|
||||
type: FieldType.NUMBER,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
description: 'Priority level for the example item (1-10)',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
export const HelloWorld = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello, World!</h1>
|
||||
<p>This is your first front component.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd371f098-5b2c-42f0-898d-94459f1ee337',
|
||||
name: 'hello-world-front-component',
|
||||
description: 'A sample front component',
|
||||
component: HelloWorld,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
return { message: 'Hello, World!' };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '2baa26eb-9aaf-4856-a4f4-30d6fd6480ee',
|
||||
name: 'hello-world-logic-function',
|
||||
description: 'A simple logic function',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '7a3f4684-51db-494d-833b-a747a3b90507',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: '1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'dfd43356-39b3-4b55-b4a7-279bec689928';
|
||||
|
||||
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd2d7f6cd-33f6-456f-bf00-17adeca926ba';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'exampleItem',
|
||||
namePlural: 'exampleItems',
|
||||
labelSingular: 'Example item',
|
||||
labelPlural: 'Example items',
|
||||
description: 'A sample custom object',
|
||||
icon: 'IconBox',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name of the example item',
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Hello world default function role',
|
||||
description: 'Hello world default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
|
||||
'd0940029-9d3c-40be-903a-52d65393028f';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-skill',
|
||||
label: 'Example Skill',
|
||||
description: 'A sample skill for your application',
|
||||
icon: 'IconBrain',
|
||||
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = 'e004df40-29f3-47ba-b39d-d3a5c444367a';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All example items',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '496c40c2-5766-419c-93bf-20fdad3f34bb',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*"],
|
||||
"exclude": ["node_modules", "dist", ".twenty"]
|
||||
}
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"application": {
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000001",
|
||||
"displayName": "Root App",
|
||||
"description": "An app with all entities at root level",
|
||||
"icon": "IconFolder",
|
||||
"defaultRoleUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000040",
|
||||
"yarnLockChecksum": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"packageJsonChecksum": "ac87abb5113d9d09be71481efc123800",
|
||||
"apiClientChecksum": null
|
||||
},
|
||||
"objects": [
|
||||
{
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"nameSingular": "myNote",
|
||||
"namePlural": "myNotes",
|
||||
"labelSingular": "My note",
|
||||
"labelPlural": "My notes",
|
||||
"description": "A simple root-level object",
|
||||
"icon": "IconNote",
|
||||
"labelIdentifierFieldMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000031",
|
||||
"fields": [
|
||||
{
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000031",
|
||||
"type": "TEXT",
|
||||
"label": "Title",
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"description": "Id",
|
||||
"icon": "Icon123",
|
||||
"isNullable": false,
|
||||
"defaultValue": "uuid",
|
||||
"type": "UUID",
|
||||
"universalIdentifier": "5bdb4f57-a349-56d0-b421-845fd55b3026"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Name",
|
||||
"description": "Name",
|
||||
"icon": "IconAbc",
|
||||
"isNullable": true,
|
||||
"defaultValue": null,
|
||||
"type": "TEXT",
|
||||
"universalIdentifier": "86cc7b06-26de-5b7b-bf4a-31fa246dfd90"
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"label": "Creation date",
|
||||
"description": "Creation date",
|
||||
"icon": "IconCalendar",
|
||||
"isNullable": false,
|
||||
"defaultValue": "now",
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "abe3bc69-1a51-5814-a722-74f5a025e92a"
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"label": "Last update",
|
||||
"description": "Last time the record was changed",
|
||||
"icon": "IconCalendarClock",
|
||||
"isNullable": false,
|
||||
"defaultValue": "now",
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "c296d408-9720-5a86-8586-3490fbf04b86"
|
||||
},
|
||||
{
|
||||
"name": "deletedAt",
|
||||
"label": "Deleted at",
|
||||
"description": "Deletion date",
|
||||
"icon": "IconCalendarClock",
|
||||
"isNullable": true,
|
||||
"defaultValue": null,
|
||||
"type": "DATE_TIME",
|
||||
"universalIdentifier": "488333a2-f4ff-5790-88ef-21f07f8f1985"
|
||||
},
|
||||
{
|
||||
"name": "createdBy",
|
||||
"label": "Created by",
|
||||
"description": "The creator of the record",
|
||||
"icon": "IconCreativeCommonsSa",
|
||||
"isNullable": false,
|
||||
"defaultValue": {
|
||||
"name": "''",
|
||||
"source": "'MANUAL'"
|
||||
},
|
||||
"type": "ACTOR",
|
||||
"universalIdentifier": "fba6e01b-6579-554e-ad64-a676003c8e68"
|
||||
},
|
||||
{
|
||||
"name": "updatedBy",
|
||||
"label": "Updated by",
|
||||
"description": "The workspace member who last updated the record",
|
||||
"icon": "IconUserCircle",
|
||||
"isNullable": false,
|
||||
"defaultValue": {
|
||||
"name": "''",
|
||||
"source": "'MANUAL'"
|
||||
},
|
||||
"type": "ACTOR",
|
||||
"universalIdentifier": "fa60f87d-15c8-5ff7-9da0-191fa14a922f"
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"label": "Position",
|
||||
"description": "Position",
|
||||
"icon": "IconHierarchy2",
|
||||
"isNullable": false,
|
||||
"defaultValue": 0,
|
||||
"type": "POSITION",
|
||||
"universalIdentifier": "4a5d7791-ddc9-53ee-bc46-699da7373290"
|
||||
},
|
||||
{
|
||||
"name": "searchVector",
|
||||
"label": "Search vector",
|
||||
"icon": "IconSearch",
|
||||
"description": "Search vector",
|
||||
"isNullable": true,
|
||||
"defaultValue": null,
|
||||
"type": "TS_VECTOR",
|
||||
"universalIdentifier": "0f00bc3b-f313-5206-8496-842af20a538a"
|
||||
},
|
||||
{
|
||||
"name": "timelineActivities",
|
||||
"label": "Timeline Activities",
|
||||
"description": "My notes tied to the MyNote",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "7e28b1ea-9a94-5d36-bc03-3f12f476867a",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5"
|
||||
},
|
||||
{
|
||||
"name": "favorites",
|
||||
"label": "Favorites",
|
||||
"description": "My notes tied to the MyNote",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "ed80c73b-25ff-5a3c-99c7-e27fa2f2611e",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "698f12fe-3feb-55e3-b6cc-73375f1a8ae6",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860"
|
||||
},
|
||||
{
|
||||
"name": "attachments",
|
||||
"label": "Attachments",
|
||||
"description": "My notes tied to the MyNote",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "56a872c8-7a1e-563d-8772-3978c2c5c220",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a"
|
||||
},
|
||||
{
|
||||
"name": "noteTargets",
|
||||
"label": "Note Targets",
|
||||
"description": "My notes tied to the MyNote",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "011595e8-7524-5ec7-9fbc-4bd3c42d7d24",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "94b6f269-fc9a-5bcd-af5b-2bdac69f276b",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400"
|
||||
},
|
||||
{
|
||||
"name": "taskTargets",
|
||||
"label": "Task Targets",
|
||||
"description": "My notes tied to the MyNote",
|
||||
"icon": "IconBuildingSkyscraper",
|
||||
"isNullable": true,
|
||||
"type": "RELATION",
|
||||
"universalSettings": {
|
||||
"relationType": "ONE_TO_MANY"
|
||||
},
|
||||
"universalIdentifier": "dd734fa3-26c3-53dd-91aa-577c313e1fa4",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "targetMyNote",
|
||||
"label": "MyNote",
|
||||
"description": "MyNote My note",
|
||||
"icon": "IconTimelineEvent",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetMyNoteId"
|
||||
},
|
||||
"universalIdentifier": "7e28b1ea-9a94-5d36-bc03-3f12f476867a",
|
||||
"objectUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1"
|
||||
},
|
||||
{
|
||||
"name": "targetMyNote",
|
||||
"label": "MyNote",
|
||||
"description": "MyNote My note",
|
||||
"icon": "IconHeart",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetMyNoteId"
|
||||
},
|
||||
"universalIdentifier": "698f12fe-3feb-55e3-b6cc-73375f1a8ae6",
|
||||
"objectUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "ed80c73b-25ff-5a3c-99c7-e27fa2f2611e",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"type": "RELATION"
|
||||
},
|
||||
{
|
||||
"name": "targetMyNote",
|
||||
"label": "MyNote",
|
||||
"description": "MyNote My note",
|
||||
"icon": "IconFileImport",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetMyNoteId"
|
||||
},
|
||||
"universalIdentifier": "721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5",
|
||||
"objectUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "56a872c8-7a1e-563d-8772-3978c2c5c220",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f634-435d-ab8d-e1168b375c69"
|
||||
},
|
||||
{
|
||||
"name": "targetMyNote",
|
||||
"label": "MyNote",
|
||||
"description": "MyNote My note",
|
||||
"icon": "IconCheckbox",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetMyNoteId"
|
||||
},
|
||||
"universalIdentifier": "94b6f269-fc9a-5bcd-af5b-2bdac69f276b",
|
||||
"objectUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "011595e8-7524-5ec7-9fbc-4bd3c42d7d24",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f635-435d-ab8d-e1168b375c70"
|
||||
},
|
||||
{
|
||||
"name": "targetMyNote",
|
||||
"label": "MyNote",
|
||||
"description": "MyNote My note",
|
||||
"icon": "IconCheckbox",
|
||||
"isNullable": true,
|
||||
"universalSettings": {
|
||||
"relationType": "MANY_TO_ONE",
|
||||
"onDelete": "SET_NULL",
|
||||
"joinColumnName": "targetMyNoteId"
|
||||
},
|
||||
"universalIdentifier": "50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3",
|
||||
"objectUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1",
|
||||
"relationTargetFieldMetadataUniversalIdentifier": "dd734fa3-26c3-53dd-91aa-577c313e1fa4",
|
||||
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
|
||||
"type": "MORPH_RELATION",
|
||||
"morphId": "20202020-f636-435d-ab8d-e1168b375c71"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000040",
|
||||
"label": "My role",
|
||||
"description": "A simple root-level role",
|
||||
"canReadAllObjectRecords": true,
|
||||
"canUpdateAllObjectRecords": false,
|
||||
"canSoftDeleteAllObjectRecords": false,
|
||||
"canDestroyAllObjectRecords": false,
|
||||
"canUpdateAllSettings": false,
|
||||
"canBeAssignedToAgents": false,
|
||||
"canBeAssignedToUsers": true,
|
||||
"canBeAssignedToApiKeys": false
|
||||
}
|
||||
],
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"logicFunctions": [
|
||||
{
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000010",
|
||||
"name": "my-function",
|
||||
"timeoutSeconds": 5,
|
||||
"httpRouteTriggerSettings": {
|
||||
"path": "/my-function",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": false
|
||||
},
|
||||
"toolInputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"handlerName": "default.config.handler",
|
||||
"sourceHandlerPath": "my.function.ts",
|
||||
"builtHandlerPath": "my.function.mjs",
|
||||
"builtHandlerChecksum": "71f0af6b9dfeef884cc81ae75ca6ecb0"
|
||||
}
|
||||
],
|
||||
"frontComponents": [
|
||||
{
|
||||
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000020",
|
||||
"name": "my-component",
|
||||
"description": "A root-level front component",
|
||||
"componentName": "MyComponent",
|
||||
"sourceComponentPath": "my.front-component.tsx",
|
||||
"builtComponentPath": "my.front-component.mjs",
|
||||
"builtComponentChecksum": "37b905c5467584f03fa5ab2c8a05bc88",
|
||||
"isHeadless": false
|
||||
}
|
||||
],
|
||||
"publicAssets": [],
|
||||
"views": [],
|
||||
"navigationMenuItems": [],
|
||||
"pageLayouts": []
|
||||
}
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 2,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
|
||||
});
|
||||
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
@@ -10,4 +10,3 @@
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
@@ -29,13 +29,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
+1
-3
@@ -1,6 +1,5 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
@@ -8,6 +7,5 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -7,4 +7,3 @@
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -236,7 +236,7 @@ const handler = async (event: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove `as any` after running `yarn twenty dev` to regenerate the typed client
|
||||
// TODO: remove `as any` after running `yarn twenty app:dev` to regenerate the typed client
|
||||
const updateSummary = async (markdown: string) => {
|
||||
await client.mutation({
|
||||
updateCallRecording: {
|
||||
|
||||
-2
@@ -1,12 +1,10 @@
|
||||
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
|
||||
name: 'Call recordings',
|
||||
icon: 'IconPhone',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: TRANSCRIPT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
name: 'transcript',
|
||||
label: 'Transcript',
|
||||
description: 'Human-readable transcript of the call',
|
||||
@@ -114,7 +114,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: SUMMARY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
name: 'summary',
|
||||
label: 'Summary',
|
||||
description: 'AI-generated summary of the call',
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Use this skill when a user asks you to summarize, analyze, or extract insights f
|
||||
|
||||
## How to Access the Data
|
||||
1. Use \`find_one_callRecording\` to fetch the call recording by its ID.
|
||||
2. Read the \`transcript\` field (RICH_TEXT, markdown format) which contains the full conversation.
|
||||
2. Read the \`transcript\` field (RICH_TEXT_V2, markdown format) which contains the full conversation.
|
||||
3. The transcript uses the format: **Speaker Name:** spoken text
|
||||
|
||||
## What to Produce
|
||||
|
||||
-2
@@ -1,5 +1,4 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
@@ -7,7 +6,6 @@ export default defineNavigationMenuItem({
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
@@ -91,6 +92,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
|
||||
@@ -89,15 +89,6 @@ password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Check if using external secret for redis password */}}
|
||||
{{- define "twenty.redis.useExternalSecret" -}}
|
||||
{{- if and (not .Values.redisInternal.enabled) .Values.redis.external.secretName .Values.redis.external.passwordKey -}}
|
||||
true
|
||||
{{- else -}}
|
||||
false
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Redis URL */}}
|
||||
{{- define "twenty.redisUrl" -}}
|
||||
{{- if .Values.server.env.REDIS_URL -}}
|
||||
@@ -108,14 +99,9 @@ false
|
||||
{{- else -}}
|
||||
{{- $host := .Values.redis.external.host | default "redis" -}}
|
||||
{{- $port := .Values.redis.external.port | default 6379 -}}
|
||||
{{- if or (eq (include "twenty.redis.useExternalSecret" .) "true") (.Values.redis.external.password) -}}
|
||||
{{- $auth := ":$(REDIS_PASSWORD)@" -}}
|
||||
{{- printf "redis://%s%s:%v" $auth $host $port -}}
|
||||
{{- else -}}
|
||||
{{- printf "redis://%s:%v" $host $port -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Server URL from override, ingress, or service */}}
|
||||
{{- define "twenty.serverUrl" -}}
|
||||
|
||||
@@ -46,12 +46,12 @@ spec:
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -83,16 +83,16 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -Atc "SELECT 1 FROM pg_database WHERE datname = :'db'" | grep -q 1 || \
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -c 'CREATE DATABASE :"db";'
|
||||
echo "Creating app user ${APP_USER} if it doesn't exist..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
echo "Creating core schema and granting permissions..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'CREATE SCHEMA IF NOT EXISTS core'
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v db="${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON DATABASE :"db" TO :"app_user";'
|
||||
@@ -106,6 +106,31 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO :"app_user";'
|
||||
echo "Database ${DBNAME} is ready."
|
||||
{{- end }}
|
||||
- name: run-migrations
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
image: {{ include "twenty.image.repository" $img }}:{{ include "twenty.image.tag" $img }}
|
||||
imagePullPolicy: {{ include "twenty.image.pullPolicy" $img }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource
|
||||
env:
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
@@ -129,16 +154,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: SIGN_IN_PREFILLED
|
||||
@@ -156,10 +171,7 @@ spec:
|
||||
key: accessToken
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http-tcp
|
||||
|
||||
@@ -67,16 +67,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: STORAGE_TYPE
|
||||
@@ -86,12 +76,9 @@ spec:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.secret.tokens.name" . }}
|
||||
key: accessToken
|
||||
{{- with .Values.worker.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- if and .Values.redisInternal.enabled .Values.redisInternal.persistence.enabled (not .Values.redisInternal.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "twenty.fullname" . }}-redis
|
||||
namespace: {{ include "twenty.namespace" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "twenty.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: redis
|
||||
spec:
|
||||
accessModes:
|
||||
{{ toYaml .Values.redisInternal.persistence.accessModes | nindent 4 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redisInternal.persistence.size }}
|
||||
{{- if .Values.redisInternal.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.redisInternal.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,58 +0,0 @@
|
||||
suite: env mapping
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: renders server extraEnv with plain value
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
|
||||
- it: renders server extraEnv with valueFrom
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
|
||||
- it: renders worker extraEnv with valueFrom
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
worker.extraEnv:
|
||||
- name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
@@ -1,89 +0,0 @@
|
||||
suite: redis external authentication
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: injects REDIS_PASSWORD from external secret into server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects REDIS_PASSWORD from external secret into worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into server when using internal redis
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into worker when using internal redis
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
@@ -105,3 +105,18 @@ tests:
|
||||
path: spec.template.spec.initContainers[?(@.name=="ensure-database-exists")].command[2]
|
||||
pattern: ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES
|
||||
|
||||
# TypeORM Migration Tests
|
||||
# ======================
|
||||
# TypeORM migrations are configured to use the core.datasource which targets the
|
||||
# 'core' schema. This ensures the _typeorm_migrations table and all application
|
||||
# tables use the dedicated core schema.
|
||||
|
||||
- it: migrations run against core datasource
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
db.enabled: true
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.initContainers[?(@.name=="run-migrations")].command[2]
|
||||
pattern: core\.datasource
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ tests:
|
||||
- crm.example.com
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
value: "https://crm.example.com:443"
|
||||
- it: falls back to service when ingress disabled
|
||||
set:
|
||||
@@ -28,7 +28,7 @@ tests:
|
||||
server.env.SERVER_URL: ""
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
pattern: ^http://my-twenty-twenty-server\.default\.svc\.cluster\.local:3000$
|
||||
---
|
||||
suite: ingress configuration
|
||||
@@ -66,7 +66,6 @@ tests:
|
||||
set:
|
||||
server.ingress.acme: true
|
||||
asserts:
|
||||
- isSubset:
|
||||
path: metadata.annotations
|
||||
content:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
- equal:
|
||||
path: metadata.annotations[cert-manager.io/cluster-issuer]
|
||||
value: letsencrypt-prod
|
||||
|
||||
@@ -61,27 +61,12 @@
|
||||
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
|
||||
"EMAIL_FROM_ADDRESS": { "type": "string" },
|
||||
"EMAIL_FROM_NAME": { "type": "string" },
|
||||
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
|
||||
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
|
||||
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
|
||||
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -157,22 +142,6 @@
|
||||
"STORAGE_TYPE": { "type": "string" },
|
||||
"DISABLE_DB_MIGRATIONS": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,14 +52,6 @@ server:
|
||||
SIGN_IN_PREFILLED: "false"
|
||||
ACCESS_TOKEN_EXPIRES_IN: "7d"
|
||||
LOGIN_TOKEN_EXPIRES_IN: "1h"
|
||||
extraEnv: []
|
||||
# - name: EMAIL_DRIVER
|
||||
# value: smtp
|
||||
# - name: SMTP_PASSWORD
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: smtp-creds
|
||||
# key: password
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -113,8 +105,6 @@ worker:
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
extraEnv: []
|
||||
|
||||
# PostgreSQL
|
||||
db:
|
||||
enabled: true
|
||||
@@ -184,6 +174,3 @@ redis:
|
||||
external:
|
||||
host: ""
|
||||
port: 6379
|
||||
password: ""
|
||||
secretName: ""
|
||||
passwordKey: ""
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
ARG APP_VERSION
|
||||
|
||||
# === Stage 1: Common dependencies ===
|
||||
FROM node:22-alpine AS common-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
|
||||
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
|
||||
# === Stage 2: Build server from source ===
|
||||
FROM common-deps AS twenty-server-build
|
||||
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx run twenty-server:build
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
|
||||
|
||||
# === Stage 3: Build frontend from source ===
|
||||
# Requires ~10GB Docker memory. To skip, pre-build on host first:
|
||||
# npx nx build twenty-front
|
||||
# The COPY below will pick up packages/twenty-front/build/ if it exists.
|
||||
FROM common-deps AS twenty-front-build
|
||||
COPY --from=twenty-server-build /app/package.json /tmp/.build-sentinel
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
RUN if [ -d /app/packages/twenty-front/build ]; then \
|
||||
echo "Using pre-built frontend from host"; \
|
||||
else \
|
||||
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
|
||||
fi
|
||||
|
||||
# === Stage 4: s6-overlay ===
|
||||
FROM alpine:3.20 AS s6-fetch
|
||||
ARG S6_OVERLAY_VERSION=3.2.0.2
|
||||
ARG TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
|
||||
else echo "x86_64" > /tmp/s6arch; fi
|
||||
RUN S6_ARCH=$(cat /tmp/s6arch) && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
|
||||
cd /tmp && \
|
||||
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
|
||||
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
|
||||
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
|
||||
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
|
||||
|
||||
# === Stage 5: Final all-in-one image ===
|
||||
FROM node:22-alpine
|
||||
|
||||
# s6-overlay
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
|
||||
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
|
||||
&& rm /tmp/s6-overlay-*.tar.xz
|
||||
|
||||
# Infrastructure: Postgres, Redis, and utilities
|
||||
RUN apk add --no-cache \
|
||||
postgresql16 postgresql16-contrib \
|
||||
redis \
|
||||
curl jq su-exec
|
||||
|
||||
# tsx for database setup scripts
|
||||
RUN npm install -g tsx
|
||||
|
||||
# Twenty application (both server and frontend built from source)
|
||||
COPY --from=twenty-server-build /app /app
|
||||
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# s6 service definitions
|
||||
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
|
||||
|
||||
# Data directories
|
||||
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
|
||||
&& chown -R postgres:postgres /data/postgres \
|
||||
&& chown 1000:1000 /data/redis /app/.local-storage
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION=0.0.0
|
||||
|
||||
# Tell s6-overlay to preserve container environment variables
|
||||
ENV S6_KEEP_ENV=1
|
||||
|
||||
# Environment defaults
|
||||
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
|
||||
SERVER_URL=http://localhost:2020 \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
STORAGE_TYPE=local \
|
||||
APP_SECRET=twenty-app-dev-secret-not-for-production \
|
||||
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
|
||||
APP_VERSION=$APP_VERSION \
|
||||
NODE_ENV=development \
|
||||
NODE_PORT=3000 \
|
||||
DISABLE_DB_MIGRATIONS=true \
|
||||
DISABLE_CRON_JOBS_REGISTRATION=true \
|
||||
IS_BILLING_ENABLED=false \
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
EXPOSE 3000
|
||||
VOLUME ["/data/postgres", "/app/.local-storage"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/bin/sh /etc/s6-overlay/scripts/init-db.sh
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Initialize PostgreSQL data directory if empty
|
||||
if [ ! -f /data/postgres/PG_VERSION ]; then
|
||||
echo "Initializing PostgreSQL data directory..."
|
||||
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
|
||||
# Allow local connections without password
|
||||
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
|
||||
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
|
||||
fi
|
||||
|
||||
exec su-exec postgres postgres -D /data/postgres \
|
||||
-c listen_addresses=localhost \
|
||||
-c unix_socket_directories=/tmp
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
exec redis-server \
|
||||
--dir /data/redis \
|
||||
--maxmemory-policy noeviction \
|
||||
--bind 127.0.0.1 \
|
||||
--protected-mode yes
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn start:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn worker:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Wait for PostgreSQL to be ready (timeout after 60s)
|
||||
echo "Waiting for PostgreSQL..."
|
||||
TRIES=0
|
||||
until su-exec postgres pg_isready -h localhost; do
|
||||
TRIES=$((TRIES + 1))
|
||||
if [ "$TRIES" -ge 120 ]; then
|
||||
echo "ERROR: PostgreSQL did not become ready within 60s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "PostgreSQL is ready."
|
||||
|
||||
# Create role if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|
||||
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|
||||
|| su-exec postgres createdb -h localhost -O twenty default
|
||||
|
||||
# Run Twenty database setup and migrations
|
||||
cd /app/packages/twenty-server
|
||||
|
||||
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
|
||||
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
echo "Database appears to be empty, running initial setup..."
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
fi
|
||||
|
||||
# Always run migrations (idempotent — skips already-applied ones)
|
||||
yarn database:migrate:prod
|
||||
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
# Only seed on first boot — check if the dev workspace already exists
|
||||
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
|
||||
|
||||
if [ "$has_workspace" = "f" ]; then
|
||||
echo "Seeding app dev data..."
|
||||
yarn command:prod workspace:seed:dev --light || true
|
||||
else
|
||||
echo "Dev workspace already seeded, skipping."
|
||||
fi
|
||||
|
||||
echo "Database initialization complete."
|
||||
@@ -29,6 +29,7 @@ COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
COPY ./packages/twenty-apps /app/packages/twenty-apps
|
||||
|
||||
RUN npx nx run twenty-server:build
|
||||
|
||||
|
||||
+3
-2
@@ -9,7 +9,7 @@ The goal here is to have a consistent codebase, which is easy to read and easy t
|
||||
|
||||
For this, it's better to be a bit more verbose than to be too concise.
|
||||
|
||||
Always keep in mind that people read code more often than they write it, especially on an open source project, where anyone can contribute.
|
||||
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
|
||||
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by linters.
|
||||
|
||||
@@ -150,7 +150,7 @@ type MyType = {
|
||||
|
||||
### Use string literals instead of enums
|
||||
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, especially with code completion.
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
|
||||
|
||||
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
|
||||
@@ -288,3 +288,4 @@ An Oxlint rule, `typescript/consistent-type-imports`, enforces the no-type impor
|
||||
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
|
||||
|
||||
To ensure your code complies with this rule, make sure to run Oxlint as part of your development workflow.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "The guide for contributors (or curious developers) who want to run
|
||||
## Prerequisites
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux and macOS">
|
||||
<Tab title="Linux and MacOS">
|
||||
|
||||
Before you can install and use Twenty, make sure you install the following on your computer:
|
||||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
@@ -31,7 +31,7 @@ wsl --install
|
||||
```
|
||||
You should now see a prompt to restart your computer. If not, restart it manually.
|
||||
|
||||
Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time.
|
||||
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
|
||||
You'll see a prompt to create a username and password for your Ubuntu installation.
|
||||
|
||||
2. Install and configure git
|
||||
@@ -104,7 +104,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Option 1 (preferred):** To provision your database locally:
|
||||
Use the following link to install PostgreSQL on your Linux machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -131,7 +131,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
```
|
||||
|
||||
The installer might not create the `postgres` user by default when installing
|
||||
via Homebrew on macOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
username (e.g., "john").
|
||||
To check and create the `postgres` user if necessary, follow these steps:
|
||||
```bash
|
||||
@@ -174,8 +174,8 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tab title="Windows (WSL)">
|
||||
All the following steps are to be run in the WSL terminal (within your virtual machine)
|
||||
|
||||
**Option 1:** To provision your PostgreSQL locally:
|
||||
Use the following link to install PostgreSQL on your Linux virtual machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
**Option 1:** To provision your Postgresql locally:
|
||||
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -190,12 +190,10 @@ You should run all commands in the following steps from the root of the project.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
You can now access the database at `localhost:5432`.
|
||||
|
||||
If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine.
|
||||
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
|
||||
|
||||
## Step 4: Set up a Redis Database (cache)
|
||||
Twenty requires a Redis cache to provide the best performance.
|
||||
Twenty requires a redis cache to provide the best performance
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
@@ -212,10 +210,8 @@ Twenty requires a Redis cache to provide the best performance.
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
Start your Redis server:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
Start your redis server:
|
||||
```brew services start redis```
|
||||
|
||||
**Option 2:** If you have docker installed:
|
||||
```bash
|
||||
@@ -233,11 +229,11 @@ Twenty requires a Redis cache to provide the best performance.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/) (free version available).
|
||||
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
|
||||
|
||||
## Step 5: Set up environment variables
|
||||
## Step 5: Setup environment variables
|
||||
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup).
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup)
|
||||
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
```bash
|
||||
|
||||
@@ -20,51 +20,19 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
|
||||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Local Server Management
|
||||
|
||||
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Start the local server (pulls the image if needed)
|
||||
yarn twenty server start
|
||||
|
||||
# Check server status
|
||||
yarn twenty server status
|
||||
|
||||
# Stream server logs
|
||||
yarn twenty server logs
|
||||
|
||||
# Stop the server
|
||||
yarn twenty server stop
|
||||
|
||||
# Reset all data and start fresh
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
The local server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`), so you can start developing immediately without any manual setup.
|
||||
|
||||
### Authentication
|
||||
|
||||
Connect your app to the local server using OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Authenticate via OAuth (opens browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
@@ -95,12 +63,6 @@ yarn twenty function:execute --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty app:build
|
||||
|
||||
# Publish the app to npm or a Twenty server
|
||||
yarn twenty app:publish
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
|
||||
@@ -1262,113 +1224,6 @@ Key points:
|
||||
|
||||
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Building your app
|
||||
|
||||
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty app:build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty app:build --tarball
|
||||
```
|
||||
|
||||
The build process:
|
||||
|
||||
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
|
||||
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
|
||||
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
|
||||
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
|
||||
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
|
||||
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
|
||||
7. **Optionally creates a tarball** — if `--tarball` is passed, runs `npm pack` to create a `.tgz` file ready for distribution.
|
||||
|
||||
The build output in `.twenty/output/` contains:
|
||||
|
||||
```text
|
||||
.twenty/output/
|
||||
├── manifest.json # Manifest with checksums for all built files
|
||||
├── package.json # Copied from app root
|
||||
├── yarn.lock # Copied from app root
|
||||
├── src/
|
||||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||||
│ └── front-components/ # Compiled .mjs front component files
|
||||
├── public/ # Static assets (if any)
|
||||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--tarball` | Also pack the output into a `.tgz` tarball |
|
||||
|
||||
## Publishing your app
|
||||
|
||||
Use `app:publish` to distribute your app — either to the npm registry or directly to a Twenty server.
|
||||
|
||||
### Publish to npm (default)
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.
|
||||
|
||||
### Publish to a Twenty server
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish directly to a Twenty server
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--server <url>` | Publish to a Twenty server instead of npm |
|
||||
| `--token <token>` | Authentication token for the target server |
|
||||
| `--tag <tag>` | npm dist-tag (e.g. `beta`, `next`) — only for npm publish |
|
||||
|
||||
## Application registration
|
||||
|
||||
Before an app can be installed in a workspace, it must be **registered**. A registration is a metadata record that describes where the app comes from and how to authenticate it. This is handled automatically by the CLI in most cases.
|
||||
|
||||
### Source types
|
||||
|
||||
Each registration has a **source type** that determines how the app's files are resolved during installation:
|
||||
|
||||
| Source type | How files are resolved | Typical use case |
|
||||
|-------------|----------------------|------------------|
|
||||
| `LOCAL` | Files are synced in real-time by the CLI watcher — installation is skipped | Development with `app:dev` |
|
||||
| `NPM` | Fetched from the npm registry via the `sourcePackage` field | Published apps on npm |
|
||||
| `TARBALL` | Extracted from an uploaded `.tgz` file stored on the server | Private apps published with `--server` |
|
||||
|
||||
### How registration happens
|
||||
|
||||
- **`app:dev`** — automatically creates a `LOCAL` registration the first time you run dev mode against a workspace.
|
||||
- **`app:publish --server`** — uploads a tarball and creates (or updates) a `TARBALL` registration, then installs the app.
|
||||
- **npm marketplace** — `NPM` registrations are created when apps are synced from the npm registry into the Twenty marketplace catalog.
|
||||
- **GraphQL API** — you can also create registrations programmatically via the `createApplicationRegistration` mutation.
|
||||
|
||||
### Registration vs installation
|
||||
|
||||
**Registration** and **installation** are separate concepts:
|
||||
|
||||
- A **registration** (`ApplicationRegistration`) is a global metadata record describing the app: its name, source type, OAuth credentials, and marketplace listing status. It exists independently of any workspace.
|
||||
- An **installation** (`Application`) is a per-workspace instance. When a user installs an app, Twenty resolves the package from the registration's source, writes the built files to storage, and synchronizes the manifest (creating objects, fields, logic functions, etc.) in that workspace.
|
||||
|
||||
One registration can be installed in many workspaces. Each workspace gets its own copy of the app's files and data model.
|
||||
|
||||
### OAuth credentials
|
||||
|
||||
Each registration includes OAuth credentials (`oAuthClientId` and `oAuthClientSecret`) generated at creation time. These are used by the app to authenticate API requests on behalf of users. The client secret is returned **once** at creation — store it securely. You can rotate it later via the `rotateApplicationRegistrationClientSecret` mutation.
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 1-Click w/ Docker Compose
|
||||
|
||||
|
||||
<Warning>
|
||||
Docker containers are for production hosting or self-hosting. For contributing, please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Overview
|
||||
@@ -13,7 +13,7 @@ This guide provides step-by-step instructions to install and configure the Twent
|
||||
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
See [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the `docker-compose.yml` file at the server and/or worker level, depending on the variable.
|
||||
See docs [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
|
||||
## System Requirements
|
||||
|
||||
@@ -237,3 +237,4 @@ docker compose up -d
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
|
||||
|
||||
|
||||
|
||||
@@ -157,9 +157,7 @@ plugins: [
|
||||
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
|
||||
|
||||
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
|
||||
|
||||
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
|
||||
|
||||
### 1-click Docker compose
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user