{
  "manifest_version": "1.0.0",
  "template": {
    "id": "4d4f8a00-d497-4541-8b58-f144dceb9c5b",
    "slug": "twenty-self-hosted-salesforce-alternativ",
    "name": "Twenty (Self-Hosted Salesforce Alternative)",
    "description": "Open-source CRM. Self-host a Salesforce alternative. [Updated Aug '26]",
    "url": "https://railway.com/deploy/twenty-self-hosted-salesforce-alternativ",
    "upstream": {
      "image": "twentycrm/twenty:v2.14.3"
    }
  },
  "services": [
    {
      "name": "minio",
      "source": {
        "image": "minio/minio:RELEASE.2025-04-08T15-41-24Z"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    },
    {
      "name": "postgres",
      "source": {
        "image": "postgres:16"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/postgresql/data",
      "http": false
    },
    {
      "name": "redis",
      "source": {
        "image": "redis:7-alpine"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    },
    {
      "name": "twenty",
      "source": {
        "image": "twentycrm/twenty:v2.14.3"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "twenty-worker",
      "source": {
        "image": "twentycrm/twenty:v2.14.3"
      },
      "needs_volume": false,
      "http": false
    }
  ],
  "required_inputs": [
    {
      "key": "MINIO_ROOT_USER",
      "service": "minio",
      "description": "MinIO root access key used by Twenty as the S3 access key ID.",
      "secret": false,
      "strategy": "default",
      "default": "minio"
    },
    {
      "key": "MINIO_ROOT_PASSWORD",
      "service": "minio",
      "description": "Auto-generated MinIO root secret used by Twenty as the S3 secret access key.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "PGDATA",
      "service": "postgres",
      "description": "Data directory inside the mounted volume.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/postgresql/data/pgdata"
    },
    {
      "key": "POSTGRES_DB",
      "service": "postgres",
      "description": "Database name. Must stay 'default' — Twenty targets /default in its connection URL.",
      "secret": false,
      "strategy": "default",
      "default": "default"
    },
    {
      "key": "POSTGRES_USER",
      "service": "postgres",
      "description": "Postgres superuser role Twenty connects as.",
      "secret": false,
      "strategy": "default",
      "default": "postgres"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "postgres",
      "description": "Auto-generated Postgres password (alphanumeric so it is URL-safe in the connection string).",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "REDIS_PASSWORD",
      "service": "redis",
      "description": "Auto-generated Redis password (alphanumeric so it is URL-safe in the Redis URL).",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "PORT",
      "service": "twenty",
      "description": "Port Railway routes the public domain to; must match NODE_PORT.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "NODE_PORT",
      "service": "twenty",
      "description": "Port the Twenty server listens on inside the container.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "REDIS_URL",
      "service": "twenty",
      "description": "Redis connection string for the job queue and cache.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "APP_SECRET",
      "service": "twenty",
      "description": "Auto-generated secret used to sign tokens and sessions.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "SERVER_URL",
      "service": "twenty",
      "description": "Public base URL of this Twenty instance, set to your Railway domain.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "STORAGE_TYPE",
      "service": "twenty",
      "description": "Use S3-compatible object storage (MinIO) so the server and worker share uploaded files.",
      "secret": false,
      "strategy": "default",
      "default": "s3"
    },
    {
      "key": "ENCRYPTION_KEY",
      "service": "twenty",
      "description": "Auto-generated key that encrypts sensitive fields at rest. Keep it stable across deploys.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "PG_DATABASE_URL",
      "service": "twenty",
      "description": "PostgreSQL connection string over Railway's private network.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "STORAGE_S3_NAME",
      "service": "twenty",
      "description": "S3 bucket name used for file storage.",
      "secret": false,
      "strategy": "default",
      "default": "twenty"
    },
    {
      "key": "STORAGE_S3_REGION",
      "service": "twenty",
      "description": "S3 region label required by the client; any value works with MinIO.",
      "secret": false,
      "strategy": "default",
      "default": "us-east-1"
    },
    {
      "key": "STORAGE_S3_ENDPOINT",
      "service": "twenty",
      "description": "Private MinIO endpoint for object storage.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DISABLE_DB_MIGRATIONS",
      "service": "twenty",
      "description": "Server runs database migrations on boot. Leave false on the server.",
      "secret": false,
      "strategy": "default",
      "default": "false"
    },
    {
      "key": "STORAGE_S3_ACCESS_KEY_ID",
      "service": "twenty",
      "description": "S3 access key, matching the MinIO root user.",
      "secret": true,
      "strategy": "default",
      "default": "minio"
    },
    {
      "key": "STORAGE_S3_SECRET_ACCESS_KEY",
      "service": "twenty",
      "description": "S3 secret key, referencing the MinIO root password.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DISABLE_CRON_JOBS_REGISTRATION",
      "service": "twenty",
      "description": "Server registers scheduled cron jobs. Leave false on the server.",
      "secret": false,
      "strategy": "default",
      "default": "false"
    },
    {
      "key": "NODE_PORT",
      "service": "twenty-worker",
      "description": "Port label for the worker process (no public domain is exposed).",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "REDIS_URL",
      "service": "twenty-worker",
      "description": "Same Redis connection string as the server.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "APP_SECRET",
      "service": "twenty-worker",
      "description": "References the server's app secret so tokens validate across both.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "SERVER_URL",
      "service": "twenty-worker",
      "description": "Public URL of the Twenty server, referenced so links in emails/jobs are correct.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "STORAGE_TYPE",
      "service": "twenty-worker",
      "description": "Use the same S3 storage backend as the server.",
      "secret": false,
      "strategy": "default",
      "default": "s3"
    },
    {
      "key": "ENCRYPTION_KEY",
      "service": "twenty-worker",
      "description": "References the server's encryption key so both decrypt the same data.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PG_DATABASE_URL",
      "service": "twenty-worker",
      "description": "Same PostgreSQL connection string as the server.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "STORAGE_S3_NAME",
      "service": "twenty-worker",
      "description": "S3 bucket name, matching the server.",
      "secret": false,
      "strategy": "default",
      "default": "twenty"
    },
    {
      "key": "STORAGE_S3_REGION",
      "service": "twenty-worker",
      "description": "S3 region label, matching the server.",
      "secret": false,
      "strategy": "default",
      "default": "us-east-1"
    },
    {
      "key": "STORAGE_S3_ENDPOINT",
      "service": "twenty-worker",
      "description": "Private MinIO endpoint, matching the server.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DISABLE_DB_MIGRATIONS",
      "service": "twenty-worker",
      "description": "Worker must NOT run migrations; only the server does.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "STORAGE_S3_ACCESS_KEY_ID",
      "service": "twenty-worker",
      "description": "S3 access key, matching the server.",
      "secret": true,
      "strategy": "default",
      "default": "minio"
    },
    {
      "key": "STORAGE_S3_SECRET_ACCESS_KEY",
      "service": "twenty-worker",
      "description": "S3 secret key, matching the server.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DISABLE_CRON_JOBS_REGISTRATION",
      "service": "twenty-worker",
      "description": "Worker processes jobs but does not register crons; the server does.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "twenty-self-hosted-salesforce-alternativ"
      }
    },
    "cli": "railway deploy --template twenty-self-hosted-salesforce-alternativ",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "4d4f8a00-d497-4541-8b58-f144dceb9c5b",
            "serializedConfig": {
              "buckets": {},
              "services": {
                "0f9fe02b-00ea-4e6d-a588-199c53c0c49e": {
                  "icon": null,
                  "name": "minio",
                  "deploy": {
                    "startCommand": "sh -c 'mkdir -p /data/twenty && minio server --address \":9000\" --console-address \":9001\" /data'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "minio/minio:RELEASE.2025-04-08T15-41-24Z"
                  },
                  "variables": {
                    "MINIO_ROOT_USER": {
                      "isOptional": false,
                      "description": "MinIO root access key used by Twenty as the S3 access key ID.",
                      "defaultValue": "minio"
                    },
                    "MINIO_ROOT_PASSWORD": {
                      "isOptional": false,
                      "description": "Auto-generated MinIO root secret used by Twenty as the S3 secret access key.",
                      "defaultValue": "{{MINIO_ROOT_PASSWORD}}"
                    }
                  },
                  "volumeMounts": {
                    "0f9fe02b-00ea-4e6d-a588-199c53c0c49e": {
                      "mountPath": "/data"
                    }
                  }
                },
                "22bd82a6-a63c-4bf9-905d-5ba1ac76f4b8": {
                  "icon": null,
                  "name": "postgres",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "postgres:16"
                  },
                  "variables": {
                    "PGDATA": {
                      "isOptional": false,
                      "description": "Data directory inside the mounted volume.",
                      "defaultValue": "/var/lib/postgresql/data/pgdata"
                    },
                    "POSTGRES_DB": {
                      "isOptional": false,
                      "description": "Database name. Must stay 'default' — Twenty targets /default in its connection URL.",
                      "defaultValue": "default"
                    },
                    "POSTGRES_USER": {
                      "isOptional": false,
                      "description": "Postgres superuser role Twenty connects as.",
                      "defaultValue": "postgres"
                    },
                    "POSTGRES_PASSWORD": {
                      "isOptional": false,
                      "description": "Auto-generated Postgres password (alphanumeric so it is URL-safe in the connection string).",
                      "defaultValue": "{{POSTGRES_PASSWORD}}"
                    }
                  },
                  "volumeMounts": {
                    "22bd82a6-a63c-4bf9-905d-5ba1ac76f4b8": {
                      "mountPath": "/var/lib/postgresql/data"
                    }
                  }
                },
                "2b2ba8da-b273-444b-9132-113f02f4e008": {
                  "icon": null,
                  "name": "redis",
                  "deploy": {
                    "startCommand": "sh -c 'redis-server --requirepass \"$REDIS_PASSWORD\" --maxmemory-policy noeviction'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "redis:7-alpine"
                  },
                  "variables": {
                    "REDIS_PASSWORD": {
                      "isOptional": false,
                      "description": "Auto-generated Redis password (alphanumeric so it is URL-safe in the Redis URL).",
                      "defaultValue": "{{REDIS_PASSWORD}}"
                    }
                  },
                  "volumeMounts": {
                    "2b2ba8da-b273-444b-9132-113f02f4e008": {
                      "mountPath": "/data"
                    }
                  }
                },
                "45af0eb7-4b53-4a1d-97d3-47d201ff77bc": {
                  "icon": null,
                  "name": "twenty",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "twentycrm/twenty:v2.14.3"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes the public domain to; must match NODE_PORT.",
                      "defaultValue": "3000"
                    },
                    "NODE_PORT": {
                      "isOptional": false,
                      "description": "Port the Twenty server listens on inside the container.",
                      "defaultValue": "3000"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Redis connection string for the job queue and cache.",
                      "defaultValue": "redis://:${{redis.REDIS_PASSWORD}}@${{redis.RAILWAY_PRIVATE_DOMAIN}}:6379"
                    },
                    "APP_SECRET": {
                      "isOptional": false,
                      "description": "Auto-generated secret used to sign tokens and sessions.",
                      "defaultValue": "{{APP_SECRET}}"
                    },
                    "SERVER_URL": {
                      "isOptional": false,
                      "description": "Public base URL of this Twenty instance, set to your Railway domain.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "STORAGE_TYPE": {
                      "isOptional": false,
                      "description": "Use S3-compatible object storage (MinIO) so the server and worker share uploaded files.",
                      "defaultValue": "s3"
                    },
                    "ENCRYPTION_KEY": {
                      "isOptional": false,
                      "description": "Auto-generated key that encrypts sensitive fields at rest. Keep it stable across deploys.",
                      "defaultValue": "{{ENCRYPTION_KEY}}"
                    },
                    "PG_DATABASE_URL": {
                      "isOptional": false,
                      "description": "PostgreSQL connection string over Railway's private network.",
                      "defaultValue": "postgres://postgres:${{postgres.POSTGRES_PASSWORD}}@${{postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/default"
                    },
                    "STORAGE_S3_NAME": {
                      "isOptional": false,
                      "description": "S3 bucket name used for file storage.",
                      "defaultValue": "twenty"
                    },
                    "STORAGE_S3_REGION": {
                      "isOptional": false,
                      "description": "S3 region label required by the client; any value works with MinIO.",
                      "defaultValue": "us-east-1"
                    },
                    "STORAGE_S3_ENDPOINT": {
                      "isOptional": false,
                      "description": "Private MinIO endpoint for object storage.",
                      "defaultValue": "http://${{minio.RAILWAY_PRIVATE_DOMAIN}}:9000"
                    },
                    "DISABLE_DB_MIGRATIONS": {
                      "isOptional": false,
                      "description": "Server runs database migrations on boot. Leave false on the server.",
                      "defaultValue": "false"
                    },
                    "STORAGE_S3_ACCESS_KEY_ID": {
                      "isOptional": false,
                      "description": "S3 access key, matching the MinIO root user.",
                      "defaultValue": "minio"
                    },
                    "STORAGE_S3_SECRET_ACCESS_KEY": {
                      "isOptional": false,
                      "description": "S3 secret key, referencing the MinIO root password.",
                      "defaultValue": "${{minio.MINIO_ROOT_PASSWORD}}"
                    },
                    "DISABLE_CRON_JOBS_REGISTRATION": {
                      "isOptional": false,
                      "description": "Server registers scheduled cron jobs. Leave false on the server.",
                      "defaultValue": "false"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>": {}
                    }
                  }
                },
                "e7202622-c1c9-4e79-9b10-59411c2bf31f": {
                  "icon": null,
                  "name": "twenty-worker",
                  "deploy": {
                    "startCommand": "yarn worker:prod",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "twentycrm/twenty:v2.14.3"
                  },
                  "variables": {
                    "NODE_PORT": {
                      "isOptional": false,
                      "description": "Port label for the worker process (no public domain is exposed).",
                      "defaultValue": "3000"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Same Redis connection string as the server.",
                      "defaultValue": "redis://:${{redis.REDIS_PASSWORD}}@${{redis.RAILWAY_PRIVATE_DOMAIN}}:6379"
                    },
                    "APP_SECRET": {
                      "isOptional": false,
                      "description": "References the server's app secret so tokens validate across both.",
                      "defaultValue": "${{twenty.APP_SECRET}}"
                    },
                    "SERVER_URL": {
                      "isOptional": false,
                      "description": "Public URL of the Twenty server, referenced so links in emails/jobs are correct.",
                      "defaultValue": "https://${{twenty.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "STORAGE_TYPE": {
                      "isOptional": false,
                      "description": "Use the same S3 storage backend as the server.",
                      "defaultValue": "s3"
                    },
                    "ENCRYPTION_KEY": {
                      "isOptional": false,
                      "description": "References the server's encryption key so both decrypt the same data.",
                      "defaultValue": "${{twenty.ENCRYPTION_KEY}}"
                    },
                    "PG_DATABASE_URL": {
                      "isOptional": false,
                      "description": "Same PostgreSQL connection string as the server.",
                      "defaultValue": "postgres://postgres:${{postgres.POSTGRES_PASSWORD}}@${{postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/default"
                    },
                    "STORAGE_S3_NAME": {
                      "isOptional": false,
                      "description": "S3 bucket name, matching the server.",
                      "defaultValue": "twenty"
                    },
                    "STORAGE_S3_REGION": {
                      "isOptional": false,
                      "description": "S3 region label, matching the server.",
                      "defaultValue": "us-east-1"
                    },
                    "STORAGE_S3_ENDPOINT": {
                      "isOptional": false,
                      "description": "Private MinIO endpoint, matching the server.",
                      "defaultValue": "http://${{minio.RAILWAY_PRIVATE_DOMAIN}}:9000"
                    },
                    "DISABLE_DB_MIGRATIONS": {
                      "isOptional": false,
                      "description": "Worker must NOT run migrations; only the server does.",
                      "defaultValue": "true"
                    },
                    "STORAGE_S3_ACCESS_KEY_ID": {
                      "isOptional": false,
                      "description": "S3 access key, matching the server.",
                      "defaultValue": "minio"
                    },
                    "STORAGE_S3_SECRET_ACCESS_KEY": {
                      "isOptional": false,
                      "description": "S3 secret key, matching the server.",
                      "defaultValue": "${{minio.MINIO_ROOT_PASSWORD}}"
                    },
                    "DISABLE_CRON_JOBS_REGISTRATION": {
                      "isOptional": false,
                      "description": "Worker processes jobs but does not register crons; the server does.",
                      "defaultValue": "true"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {},
  "resources": {
    "expected_services": 5,
    "needs_volume": true
  },
  "generated_at": "2026-09-09T06:02:34.665Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-04T06:32:10.710Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_cf157f80c387421e8dbc",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 118,
    "typical_build_seconds": 0,
    "typical_start_seconds": 10,
    "slowest_service": "twenty-worker"
  }
}
