{
  "manifest_version": "1.0.0",
  "template": {
    "id": "de2ff91c-0616-442c-aff1-df4f7ced8774",
    "slug": "plane-project-management",
    "name": "Plane | Open Source Jira and Linear Alternative",
    "description": "Self-hosted Jira alternative: full production split, ~1.1 GB RAM",
    "url": "https://railway.com/deploy/plane-project-management",
    "upstream": {
      "image": "caddy:2.11.4-alpine"
    }
  },
  "services": [
    {
      "name": "Space",
      "source": {
        "image": "makeplane/plane-space:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Worker",
      "source": {
        "image": "makeplane/plane-backend:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Postgres",
      "source": {
        "image": "postgres:18.6-alpine"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/postgresql",
      "http": false
    },
    {
      "name": "Web",
      "source": {
        "image": "makeplane/plane-frontend:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "RabbitMQ",
      "source": {
        "image": "rabbitmq:4.3.5-alpine"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Proxy",
      "source": {
        "image": "caddy:2.11.4-alpine"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "Beat",
      "source": {
        "image": "makeplane/plane-backend:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Live",
      "source": {
        "image": "makeplane/plane-live:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Admin",
      "source": {
        "image": "makeplane/plane-admin:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Api",
      "source": {
        "image": "makeplane/plane-backend:v1.4.2"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Valkey",
      "source": {
        "image": "valkey/valkey:9.1.2-alpine"
      },
      "needs_volume": false,
      "http": false
    }
  ],
  "required_inputs": [
    {
      "key": "HOST",
      "service": "Space",
      "description": "Listen address. IPv6 wildcard stays dual-stack under Node, so the proxy reaches it over the private network and the health check still works.",
      "secret": false,
      "strategy": "default",
      "default": "::"
    },
    {
      "key": "PORT",
      "service": "Space",
      "description": "Port Railway routes traffic and health checks to.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Space",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Worker",
      "description": "Not served — this container runs Celery, not a web server. Kept so the worker reads the same configuration as the API.",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "DEBUG",
      "service": "Worker",
      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "WEB_URL",
      "service": "Worker",
      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AMQP_URL",
      "service": "Worker",
      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_URL",
      "service": "Worker",
      "description": "Valkey connection string — cache, sessions and the live server's bus.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "USE_MINIO",
      "service": "Worker",
      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "APP_DOMAIN",
      "service": "Worker",
      "description": "Host part of the public address.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_REGION",
      "service": "Worker",
      "description": "Bucket region.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "SECRET_KEY",
      "service": "Worker",
      "description": "Must be the same value the API uses — this is the same Django project.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DATABASE_URL",
      "service": "Worker",
      "description": "Postgres connection string, wired to the database in this project.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_HOST",
      "service": "Worker",
      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_PORT",
      "service": "Worker",
      "description": "Port the start command waits on before starting Django.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "MQ_WAIT_HOST",
      "service": "Worker",
      "description": "Read by the start command, which waits for RabbitMQ before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "MQ_WAIT_PORT",
      "service": "Worker",
      "description": "Port the start command waits on for RabbitMQ.",
      "secret": false,
      "strategy": "default",
      "default": "5672"
    },
    {
      "key": "ENABLE_SIGNUP",
      "service": "Worker",
      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_CONFIG_FILE",
      "service": "Worker",
      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
      "secret": false,
      "strategy": "default",
      "default": "/code/aws-config"
    },
    {
      "key": "FILE_SIZE_LIMIT",
      "service": "Worker",
      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
      "secret": false,
      "strategy": "default",
      "default": "5242880"
    },
    {
      "key": "REDIS_WAIT_HOST",
      "service": "Worker",
      "description": "Read by the start command, which waits for Valkey before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_WAIT_PORT",
      "service": "Worker",
      "description": "Port the start command waits on for Valkey.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    },
    {
      "key": "GUNICORN_WORKERS",
      "service": "Worker",
      "description": "Unused here; the worker runs Celery. Kept for configuration parity.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_ACCESS_KEY_ID",
      "service": "Worker",
      "description": "Bucket access key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "MACHINE_SIGNATURE",
      "service": "Worker",
      "description": "Shared with the API.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "API_KEY_RATE_LIMIT",
      "service": "Worker",
      "description": "Per-key limit on the public REST API.",
      "secret": true,
      "strategy": "default",
      "default": "60/minute"
    },
    {
      "key": "AWS_S3_BUCKET_NAME",
      "service": "Worker",
      "description": "Bucket holding uploads.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CELERY_CONCURRENCY",
      "service": "Worker",
      "description": "Background job workers. Celery's own default is one per CPU the container can see, which on a shared host is a lot of idle processes.",
      "secret": false,
      "strategy": "default",
      "default": "2"
    },
    {
      "key": "AWS_S3_ENDPOINT_URL",
      "service": "Worker",
      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CORS_ALLOWED_ORIGINS",
      "service": "Worker",
      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_SECRET_ACCESS_KEY",
      "service": "Worker",
      "description": "Bucket secret key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ENABLE_EMAIL_PASSWORD",
      "service": "Worker",
      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
      "secret": true,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "SIGNED_URL_EXPIRATION",
      "service": "Worker",
      "description": "How long an upload or download link stays valid, in seconds.",
      "secret": false,
      "strategy": "default",
      "default": "3600"
    },
    {
      "key": "LIVE_SERVER_SECRET_KEY",
      "service": "Worker",
      "description": "Shared with the API and the live server.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AWS_S3_ADDRESSING_STYLE",
      "service": "Worker",
      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
      "secret": false,
      "strategy": "default",
      "default": "path"
    },
    {
      "key": "ENABLE_MAGIC_LINK_LOGIN",
      "service": "Worker",
      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "AUTHENTICATION_RATE_LIMIT",
      "service": "Worker",
      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
      "secret": false,
      "strategy": "default",
      "default": "10/minute"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Worker",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Postgres",
      "description": "Port Postgres listens on inside the private network.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "POSTGRES_DB",
      "service": "Postgres",
      "description": "Database name. It is baked into the API's connection string — renaming it here alone breaks every service.",
      "secret": false,
      "strategy": "default",
      "default": "plane"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Postgres",
      "description": "Database user.",
      "secret": false,
      "strategy": "default",
      "default": "postgres"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Postgres",
      "description": "Generated once for this deployment. Letters and digits only, because it is embedded in a connection URL.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "PORT",
      "service": "Web",
      "description": "Port Railway routes traffic and health checks to.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Web",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "RabbitMQ",
      "description": "Port RabbitMQ listens on inside the private network.",
      "secret": false,
      "strategy": "default",
      "default": "5672"
    },
    {
      "key": "RABBITMQ_NODENAME",
      "service": "RabbitMQ",
      "description": "Pinned. The default is rabbit@$HOSTNAME and Railway hands out a new hostname on every deploy, so the broker would log a fresh node identity each boot.",
      "secret": false,
      "strategy": "default",
      "default": "rabbit@localhost"
    },
    {
      "key": "RABBITMQ_EXTRA_CONF",
      "service": "RabbitMQ",
      "description": "Written into the broker's conf.d at boot. RabbitMQ 4 refuses transient non-exclusive queues by default and Celery's remote-control mailbox is exactly one of those, so without this the worker cannot declare its queue and never runs a single job.",
      "secret": false,
      "strategy": "default",
      "default": "deprecated_features.permit.transient_nonexcl_queues = true"
    },
    {
      "key": "RABBITMQ_DEFAULT_PASS",
      "service": "RabbitMQ",
      "description": "Generated once for this deployment.",
      "secret": false,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "RABBITMQ_DEFAULT_USER",
      "service": "RabbitMQ",
      "description": "Broker user. It is baked into the API's AMQP URL.",
      "secret": false,
      "strategy": "default",
      "default": "plane"
    },
    {
      "key": "RABBITMQ_DEFAULT_VHOST",
      "service": "RabbitMQ",
      "description": "Broker virtual host, also baked into the AMQP URL.",
      "secret": false,
      "strategy": "default",
      "default": "plane"
    },
    {
      "key": "PORT",
      "service": "Proxy",
      "description": "Port Railway routes traffic and health checks to.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "CADDYFILE",
      "service": "Proxy",
      "description": "The whole routing table. Plane is six HTTP apps sharing one origin by path and Railway routes by service, so this file is what puts them back together: /god-mode to the admin app, /spaces to public boards, /live to collaborative editing, /api and /auth to the backend, everything else to the web app.",
      "secret": false,
      "strategy": "default",
      "default": "{\n\tadmin off\n\tauto_https off\n\tservers {\n\t\tmax_header_size 25MB\n\t\tclient_ip_headers X-Forwarded-For X-Real-IP\n\t\ttrusted_proxies static 0.0.0.0/0 ::/0\n\t}\n}\n\n:{$PORT} {\n\trequest_body {\n\t\tmax_size {$FILE_SIZE_LIMIT}\n\t}\n\n\thandle /healthz {\n\t\trespond \"ok\" 200\n\t}\n\n\tredir /spaces /spaces/ permanent\n\tredir /god-mode /god-mode/ permanent\n\n\thandle /spaces/* {\n\t\treverse_proxy {$SPACE_UPSTREAM}\n\t}\n\thandle /god-mode/* {\n\t\treverse_proxy {$ADMIN_UPSTREAM}\n\t}\n\thandle /live/* {\n\t\treverse_proxy {$LIVE_UPSTREAM}\n\t}\n\thandle /api/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle /auth/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle /static/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle {\n\t\treverse_proxy {$WEB_UPSTREAM}\n\t}\n}\n"
    },
    {
      "key": "API_UPSTREAM",
      "service": "Proxy",
      "description": "Private address of the API.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "WEB_UPSTREAM",
      "service": "Proxy",
      "description": "Private address of the web app.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "LIVE_UPSTREAM",
      "service": "Proxy",
      "description": "Private address of the collaborative editing server.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "ADMIN_UPSTREAM",
      "service": "Proxy",
      "description": "Private address of the admin app.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "SPACE_UPSTREAM",
      "service": "Proxy",
      "description": "Private address of the public-boards app.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "FILE_SIZE_LIMIT",
      "service": "Proxy",
      "description": "Largest request body the proxy accepts, in bytes. Keep it equal to the API's FILE_SIZE_LIMIT or uploads fail at whichever limit is lower.",
      "secret": false,
      "strategy": "default",
      "default": "5242880"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Proxy",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Beat",
      "description": "Not served — this container runs the Celery scheduler. Kept so it reads the same configuration as the API.",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "DEBUG",
      "service": "Beat",
      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "WEB_URL",
      "service": "Beat",
      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AMQP_URL",
      "service": "Beat",
      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_URL",
      "service": "Beat",
      "description": "Valkey connection string — cache, sessions and the live server's bus.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "USE_MINIO",
      "service": "Beat",
      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "APP_DOMAIN",
      "service": "Beat",
      "description": "Host part of the public address.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_REGION",
      "service": "Beat",
      "description": "Bucket region.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "SECRET_KEY",
      "service": "Beat",
      "description": "Must be the same value the API uses — this is the same Django project.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DATABASE_URL",
      "service": "Beat",
      "description": "Postgres connection string, wired to the database in this project.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_HOST",
      "service": "Beat",
      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_PORT",
      "service": "Beat",
      "description": "Port the start command waits on before starting Django.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "MQ_WAIT_HOST",
      "service": "Beat",
      "description": "Read by the start command, which waits for RabbitMQ before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "MQ_WAIT_PORT",
      "service": "Beat",
      "description": "Port the start command waits on for RabbitMQ.",
      "secret": false,
      "strategy": "default",
      "default": "5672"
    },
    {
      "key": "ENABLE_SIGNUP",
      "service": "Beat",
      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_CONFIG_FILE",
      "service": "Beat",
      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
      "secret": false,
      "strategy": "default",
      "default": "/code/aws-config"
    },
    {
      "key": "FILE_SIZE_LIMIT",
      "service": "Beat",
      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
      "secret": false,
      "strategy": "default",
      "default": "5242880"
    },
    {
      "key": "REDIS_WAIT_HOST",
      "service": "Beat",
      "description": "Read by the start command, which waits for Valkey before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_WAIT_PORT",
      "service": "Beat",
      "description": "Port the start command waits on for Valkey.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    },
    {
      "key": "GUNICORN_WORKERS",
      "service": "Beat",
      "description": "Unused here; this container runs the Celery scheduler.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_ACCESS_KEY_ID",
      "service": "Beat",
      "description": "Bucket access key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "MACHINE_SIGNATURE",
      "service": "Beat",
      "description": "Shared with the API.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "API_KEY_RATE_LIMIT",
      "service": "Beat",
      "description": "Per-key limit on the public REST API.",
      "secret": true,
      "strategy": "default",
      "default": "60/minute"
    },
    {
      "key": "AWS_S3_BUCKET_NAME",
      "service": "Beat",
      "description": "Bucket holding uploads.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CELERY_CONCURRENCY",
      "service": "Beat",
      "description": "Unused here; the scheduler queues work rather than running it.",
      "secret": false,
      "strategy": "default",
      "default": "2"
    },
    {
      "key": "AWS_S3_ENDPOINT_URL",
      "service": "Beat",
      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CORS_ALLOWED_ORIGINS",
      "service": "Beat",
      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_SECRET_ACCESS_KEY",
      "service": "Beat",
      "description": "Bucket secret key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ENABLE_EMAIL_PASSWORD",
      "service": "Beat",
      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
      "secret": true,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "SIGNED_URL_EXPIRATION",
      "service": "Beat",
      "description": "How long an upload or download link stays valid, in seconds.",
      "secret": false,
      "strategy": "default",
      "default": "3600"
    },
    {
      "key": "LIVE_SERVER_SECRET_KEY",
      "service": "Beat",
      "description": "Shared with the API and the live server.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AWS_S3_ADDRESSING_STYLE",
      "service": "Beat",
      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
      "secret": false,
      "strategy": "default",
      "default": "path"
    },
    {
      "key": "ENABLE_MAGIC_LINK_LOGIN",
      "service": "Beat",
      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "AUTHENTICATION_RATE_LIMIT",
      "service": "Beat",
      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
      "secret": false,
      "strategy": "default",
      "default": "10/minute"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Beat",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Live",
      "description": "Port Railway routes traffic and health checks to.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "REDIS_URL",
      "service": "Live",
      "description": "Valkey connection, used to fan document updates out between connections.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "API_BASE_URL",
      "service": "Live",
      "description": "Private address of the API, which this server calls to authorise each editing session.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "LIVE_BASE_PATH",
      "service": "Live",
      "description": "Path this server is mounted on. The proxy routes /live/* here and the health check lives at /live/health, so changing it means changing both.",
      "secret": false,
      "strategy": "default",
      "default": "/live"
    },
    {
      "key": "CORS_ALLOWED_ORIGINS",
      "service": "Live",
      "description": "Browser origins allowed to open an editing session. This server defaults it to the empty string and then reads that as a one-entry allowlist containing \"\" — which refuses every browser.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "LIVE_SERVER_SECRET_KEY",
      "service": "Live",
      "description": "Shared secret with the API. If the two differ, opening a page for editing fails to authenticate.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Live",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Admin",
      "description": "Port Railway routes traffic and health checks to.",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Admin",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Api",
      "description": "Port Railway routes traffic and health checks to. Gunicorn binds [::] on this port, which is dual-stack, so both the health check and the private network are answered.",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "DEBUG",
      "service": "Api",
      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "WEB_URL",
      "service": "Api",
      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AMQP_URL",
      "service": "Api",
      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_URL",
      "service": "Api",
      "description": "Valkey connection string — cache, sessions and the live server's bus.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "USE_MINIO",
      "service": "Api",
      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "APP_DOMAIN",
      "service": "Api",
      "description": "Host part of the public address.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_REGION",
      "service": "Api",
      "description": "Bucket region.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "SECRET_KEY",
      "service": "Api",
      "description": "Django's signing key: sessions, password reset links and the encrypted instance configuration. Changing it logs everyone out and makes the stored admin configuration unreadable. Generated once for this deployment.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "DATABASE_URL",
      "service": "Api",
      "description": "Postgres connection string, wired to the database in this project.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_HOST",
      "service": "Api",
      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DB_WAIT_PORT",
      "service": "Api",
      "description": "Port the start command waits on before starting Django.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "MQ_WAIT_HOST",
      "service": "Api",
      "description": "Read by the start command, which waits for RabbitMQ before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "MQ_WAIT_PORT",
      "service": "Api",
      "description": "Port the start command waits on for RabbitMQ.",
      "secret": false,
      "strategy": "default",
      "default": "5672"
    },
    {
      "key": "ENABLE_SIGNUP",
      "service": "Api",
      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_CONFIG_FILE",
      "service": "Api",
      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
      "secret": false,
      "strategy": "default",
      "default": "/code/aws-config"
    },
    {
      "key": "FILE_SIZE_LIMIT",
      "service": "Api",
      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
      "secret": false,
      "strategy": "default",
      "default": "5242880"
    },
    {
      "key": "REDIS_WAIT_HOST",
      "service": "Api",
      "description": "Read by the start command, which waits for Valkey before booting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_WAIT_PORT",
      "service": "Api",
      "description": "Port the start command waits on for Valkey.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    },
    {
      "key": "GUNICORN_WORKERS",
      "service": "Api",
      "description": "API worker processes. Two or three suit a busy team; each one costs roughly 150 MB.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "AWS_ACCESS_KEY_ID",
      "service": "Api",
      "description": "Bucket access key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "MACHINE_SIGNATURE",
      "service": "Api",
      "description": "Identifies this installation on first boot. Upstream derives it from the container's hostname, CPU and disk, which on Railway is a new value on every deploy; this is a stable one.",
      "secret": false,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "API_KEY_RATE_LIMIT",
      "service": "Api",
      "description": "Per-key limit on the public REST API.",
      "secret": true,
      "strategy": "default",
      "default": "60/minute"
    },
    {
      "key": "AWS_S3_BUCKET_NAME",
      "service": "Api",
      "description": "Bucket holding uploads.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CELERY_CONCURRENCY",
      "service": "Api",
      "description": "Background job workers. Celery's own default is one per CPU the container can see, which on a shared host is a lot of idle processes.",
      "secret": false,
      "strategy": "default",
      "default": "2"
    },
    {
      "key": "AWS_S3_ENDPOINT_URL",
      "service": "Api",
      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CORS_ALLOWED_ORIGINS",
      "service": "Api",
      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AWS_SECRET_ACCESS_KEY",
      "service": "Api",
      "description": "Bucket secret key.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ENABLE_EMAIL_PASSWORD",
      "service": "Api",
      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
      "secret": true,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "SIGNED_URL_EXPIRATION",
      "service": "Api",
      "description": "How long an upload or download link stays valid, in seconds.",
      "secret": false,
      "strategy": "default",
      "default": "3600"
    },
    {
      "key": "LIVE_SERVER_SECRET_KEY",
      "service": "Api",
      "description": "Shared secret between the API and the live collaboration server. The two must match or page editing fails to authenticate.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "AWS_S3_ADDRESSING_STYLE",
      "service": "Api",
      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
      "secret": false,
      "strategy": "default",
      "default": "path"
    },
    {
      "key": "ENABLE_MAGIC_LINK_LOGIN",
      "service": "Api",
      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "AUTHENTICATION_RATE_LIMIT",
      "service": "Api",
      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
      "secret": false,
      "strategy": "default",
      "default": "10/minute"
    },
    {
      "key": "ENABLE_ALPINE_PRIVATE_NETWORKING",
      "service": "Api",
      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "PORT",
      "service": "Valkey",
      "description": "Port Valkey listens on inside the private network.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "plane-project-management"
      }
    },
    "cli": "railway deploy --template plane-project-management",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "de2ff91c-0616-442c-aff1-df4f7ced8774",
            "serializedConfig": {
              "buckets": {
                "1713ee67-28e6-492b-9c15-a435a8b578c2": {
                  "name": "Bucket"
                }
              },
              "services": {
                "226daf90-a7db-4103-a478-760d7da0bd1b": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Space",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 3
                  },
                  "source": {
                    "image": "makeplane/plane-space:v1.4.2"
                  },
                  "variables": {
                    "HOST": {
                      "isOptional": false,
                      "description": "Listen address. IPv6 wildcard stays dual-stack under Node, so the proxy reaches it over the private network and the health check still works.",
                      "defaultValue": "::"
                    },
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to.",
                      "defaultValue": "3000"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "456c8a0a-5c28-4e19-8e13-3881f8a43f1d": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Worker",
                  "deploy": {
                    "startCommand": "bash -c 'set -e; for i in $(seq 1 90); do (echo > /dev/tcp/$DB_WAIT_HOST/$DB_WAIT_PORT) 2>/dev/null && break; echo \"waiting for postgres ($i/90)\"; sleep 2; done; printf \"[default]\\ns3 =\\n    addressing_style = %s\\n\" \"$AWS_S3_ADDRESSING_STYLE\" > \"$AWS_CONFIG_FILE\"; python manage.py wait_for_db; python manage.py wait_for_migrations; exec celery -A plane worker -l info --concurrency \"$CELERY_CONCURRENCY\"'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "makeplane/plane-backend:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Not served — this container runs Celery, not a web server. Kept so the worker reads the same configuration as the API.",
                      "defaultValue": "8000"
                    },
                    "DEBUG": {
                      "isOptional": false,
                      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
                      "defaultValue": "0"
                    },
                    "WEB_URL": {
                      "isOptional": false,
                      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AMQP_URL": {
                      "isOptional": false,
                      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
                      "defaultValue": "amqp://plane:${{RabbitMQ.RABBITMQ_DEFAULT_PASS}}@${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}:5672/plane"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Valkey connection string — cache, sessions and the live server's bus.",
                      "defaultValue": "redis://${{Valkey.RAILWAY_PRIVATE_DOMAIN}}:6379/"
                    },
                    "USE_MINIO": {
                      "isOptional": false,
                      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
                      "defaultValue": "0"
                    },
                    "APP_DOMAIN": {
                      "isOptional": false,
                      "description": "Host part of the public address.",
                      "defaultValue": "${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_REGION": {
                      "isOptional": false,
                      "description": "Bucket region.",
                      "defaultValue": "${{Bucket.REGION}}"
                    },
                    "SECRET_KEY": {
                      "isOptional": false,
                      "description": "Must be the same value the API uses — this is the same Django project.",
                      "defaultValue": "${{ Api.SECRET_KEY }}"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "Postgres connection string, wired to the database in this project.",
                      "defaultValue": "postgresql://postgres:${{Postgres.POSTGRES_PASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/plane"
                    },
                    "DB_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "DB_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on before starting Django.",
                      "defaultValue": "5432"
                    },
                    "MQ_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for RabbitMQ before booting.",
                      "defaultValue": "${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "MQ_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for RabbitMQ.",
                      "defaultValue": "5672"
                    },
                    "ENABLE_SIGNUP": {
                      "isOptional": false,
                      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
                      "defaultValue": "1"
                    },
                    "AWS_CONFIG_FILE": {
                      "isOptional": false,
                      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
                      "defaultValue": "/code/aws-config"
                    },
                    "FILE_SIZE_LIMIT": {
                      "isOptional": false,
                      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
                      "defaultValue": "5242880"
                    },
                    "REDIS_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Valkey before booting.",
                      "defaultValue": "${{Valkey.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "REDIS_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for Valkey.",
                      "defaultValue": "6379"
                    },
                    "GUNICORN_WORKERS": {
                      "isOptional": false,
                      "description": "Unused here; the worker runs Celery. Kept for configuration parity.",
                      "defaultValue": "1"
                    },
                    "AWS_ACCESS_KEY_ID": {
                      "isOptional": false,
                      "description": "Bucket access key.",
                      "defaultValue": "${{Bucket.ACCESS_KEY_ID}}"
                    },
                    "MACHINE_SIGNATURE": {
                      "isOptional": false,
                      "description": "Shared with the API.",
                      "defaultValue": "${{ Api.MACHINE_SIGNATURE }}"
                    },
                    "API_KEY_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-key limit on the public REST API.",
                      "defaultValue": "60/minute"
                    },
                    "AWS_S3_BUCKET_NAME": {
                      "isOptional": false,
                      "description": "Bucket holding uploads.",
                      "defaultValue": "${{Bucket.BUCKET}}"
                    },
                    "CELERY_CONCURRENCY": {
                      "isOptional": false,
                      "description": "Background job workers. Celery's own default is one per CPU the container can see, which on a shared host is a lot of idle processes.",
                      "defaultValue": "2"
                    },
                    "AWS_S3_ENDPOINT_URL": {
                      "isOptional": false,
                      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
                      "defaultValue": "${{Bucket.ENDPOINT}}"
                    },
                    "CORS_ALLOWED_ORIGINS": {
                      "isOptional": false,
                      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_SECRET_ACCESS_KEY": {
                      "isOptional": false,
                      "description": "Bucket secret key.",
                      "defaultValue": "${{Bucket.SECRET_ACCESS_KEY}}"
                    },
                    "ENABLE_EMAIL_PASSWORD": {
                      "isOptional": false,
                      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
                      "defaultValue": "1"
                    },
                    "SIGNED_URL_EXPIRATION": {
                      "isOptional": false,
                      "description": "How long an upload or download link stays valid, in seconds.",
                      "defaultValue": "3600"
                    },
                    "LIVE_SERVER_SECRET_KEY": {
                      "isOptional": false,
                      "description": "Shared with the API and the live server.",
                      "defaultValue": "${{ Api.LIVE_SERVER_SECRET_KEY }}"
                    },
                    "AWS_S3_ADDRESSING_STYLE": {
                      "isOptional": false,
                      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
                      "defaultValue": "path"
                    },
                    "ENABLE_MAGIC_LINK_LOGIN": {
                      "isOptional": false,
                      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
                      "defaultValue": "0"
                    },
                    "AUTHENTICATION_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
                      "defaultValue": "10/minute"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "4727c72e-a493-48fc-a2c3-15b2d91c6947": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/postgresql.svg",
                  "name": "Postgres",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 2
                  },
                  "source": {
                    "image": "postgres:18.6-alpine"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Postgres listens on inside the private network.",
                      "defaultValue": "5432"
                    },
                    "POSTGRES_DB": {
                      "isOptional": false,
                      "description": "Database name. It is baked into the API's connection string — renaming it here alone breaks every service.",
                      "defaultValue": "plane"
                    },
                    "POSTGRES_USER": {
                      "isOptional": false,
                      "description": "Database user.",
                      "defaultValue": "postgres"
                    },
                    "POSTGRES_PASSWORD": {
                      "isOptional": false,
                      "description": "Generated once for this deployment. Letters and digits only, because it is embedded in a connection URL.",
                      "defaultValue": "{{POSTGRES_PASSWORD}}"
                    }
                  },
                  "volumeMounts": {
                    "4727c72e-a493-48fc-a2c3-15b2d91c6947": {
                      "mountPath": "/var/lib/postgresql"
                    }
                  }
                },
                "4f61e23c-2599-4ea1-9ab3-44bc6ecb2746": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Web",
                  "deploy": {
                    "startCommand": "sh -c 'sed -i -e \"s/^worker_processes .*/worker_processes 2;/\" -e \"s|listen 3000;|listen 3000;\\n    listen [::]:3000;\\n    absolute_redirect off;\\n    port_in_redirect off;|\" /etc/nginx/nginx.conf; nginx -t; exec nginx -g \"daemon off;\"'",
                    "healthcheckPath": "/",
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 3
                  },
                  "source": {
                    "image": "makeplane/plane-frontend:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to.",
                      "defaultValue": "3000"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "5c060adc-6c6c-4cab-a9b2-c77a866655f0": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/rabbitmq.svg",
                  "name": "RabbitMQ",
                  "deploy": {
                    "startCommand": "sh -c 'printf \"%s\\n\" \"$RABBITMQ_EXTRA_CONF\" > /etc/rabbitmq/conf.d/30-plane.conf; exec /usr/local/bin/docker-entrypoint.sh rabbitmq-server'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 2
                  },
                  "source": {
                    "image": "rabbitmq:4.3.5-alpine"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port RabbitMQ listens on inside the private network.",
                      "defaultValue": "5672"
                    },
                    "RABBITMQ_NODENAME": {
                      "isOptional": false,
                      "description": "Pinned. The default is rabbit@$HOSTNAME and Railway hands out a new hostname on every deploy, so the broker would log a fresh node identity each boot.",
                      "defaultValue": "rabbit@localhost"
                    },
                    "RABBITMQ_EXTRA_CONF": {
                      "isOptional": false,
                      "description": "Written into the broker's conf.d at boot. RabbitMQ 4 refuses transient non-exclusive queues by default and Celery's remote-control mailbox is exactly one of those, so without this the worker cannot declare its queue and never runs a single job.",
                      "defaultValue": "deprecated_features.permit.transient_nonexcl_queues = true"
                    },
                    "RABBITMQ_DEFAULT_PASS": {
                      "isOptional": false,
                      "description": "Generated once for this deployment.",
                      "defaultValue": "{{RABBITMQ_DEFAULT_PASS}}"
                    },
                    "RABBITMQ_DEFAULT_USER": {
                      "isOptional": false,
                      "description": "Broker user. It is baked into the API's AMQP URL.",
                      "defaultValue": "plane"
                    },
                    "RABBITMQ_DEFAULT_VHOST": {
                      "isOptional": false,
                      "description": "Broker virtual host, also baked into the AMQP URL.",
                      "defaultValue": "plane"
                    }
                  }
                },
                "69b7b793-4c99-42d0-b7a7-e70dda1238a0": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/caddy.svg",
                  "name": "Proxy",
                  "deploy": {
                    "startCommand": "sh -c 'printf \"%s\" \"$CADDYFILE\" > /etc/caddy/Caddyfile; caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile; exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile'",
                    "healthcheckPath": "/healthz",
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "caddy:2.11.4-alpine"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to.",
                      "defaultValue": "8080"
                    },
                    "CADDYFILE": {
                      "isOptional": false,
                      "description": "The whole routing table. Plane is six HTTP apps sharing one origin by path and Railway routes by service, so this file is what puts them back together: /god-mode to the admin app, /spaces to public boards, /live to collaborative editing, /api and /auth to the backend, everything else to the web app.",
                      "defaultValue": "{\n\tadmin off\n\tauto_https off\n\tservers {\n\t\tmax_header_size 25MB\n\t\tclient_ip_headers X-Forwarded-For X-Real-IP\n\t\ttrusted_proxies static 0.0.0.0/0 ::/0\n\t}\n}\n\n:{$PORT} {\n\trequest_body {\n\t\tmax_size {$FILE_SIZE_LIMIT}\n\t}\n\n\thandle /healthz {\n\t\trespond \"ok\" 200\n\t}\n\n\tredir /spaces /spaces/ permanent\n\tredir /god-mode /god-mode/ permanent\n\n\thandle /spaces/* {\n\t\treverse_proxy {$SPACE_UPSTREAM}\n\t}\n\thandle /god-mode/* {\n\t\treverse_proxy {$ADMIN_UPSTREAM}\n\t}\n\thandle /live/* {\n\t\treverse_proxy {$LIVE_UPSTREAM}\n\t}\n\thandle /api/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle /auth/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle /static/* {\n\t\treverse_proxy {$API_UPSTREAM}\n\t}\n\thandle {\n\t\treverse_proxy {$WEB_UPSTREAM}\n\t}\n}\n"
                    },
                    "API_UPSTREAM": {
                      "isOptional": false,
                      "description": "Private address of the API.",
                      "defaultValue": "${{Api.RAILWAY_PRIVATE_DOMAIN}}:8000"
                    },
                    "WEB_UPSTREAM": {
                      "isOptional": false,
                      "description": "Private address of the web app.",
                      "defaultValue": "${{Web.RAILWAY_PRIVATE_DOMAIN}}:3000"
                    },
                    "LIVE_UPSTREAM": {
                      "isOptional": false,
                      "description": "Private address of the collaborative editing server.",
                      "defaultValue": "${{Live.RAILWAY_PRIVATE_DOMAIN}}:3000"
                    },
                    "ADMIN_UPSTREAM": {
                      "isOptional": false,
                      "description": "Private address of the admin app.",
                      "defaultValue": "${{Admin.RAILWAY_PRIVATE_DOMAIN}}:3000"
                    },
                    "SPACE_UPSTREAM": {
                      "isOptional": false,
                      "description": "Private address of the public-boards app.",
                      "defaultValue": "${{Space.RAILWAY_PRIVATE_DOMAIN}}:3000"
                    },
                    "FILE_SIZE_LIMIT": {
                      "isOptional": false,
                      "description": "Largest request body the proxy accepts, in bytes. Keep it equal to the API's FILE_SIZE_LIMIT or uploads fail at whichever limit is lower.",
                      "defaultValue": "5242880"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>:8080": {
                        "port": 8080
                      }
                    }
                  }
                },
                "6daaef38-84db-4ee6-b867-703eb1d13102": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Beat",
                  "deploy": {
                    "startCommand": "bash -c 'set -e; for i in $(seq 1 90); do (echo > /dev/tcp/$DB_WAIT_HOST/$DB_WAIT_PORT) 2>/dev/null && break; echo \"waiting for postgres ($i/90)\"; sleep 2; done; printf \"[default]\\ns3 =\\n    addressing_style = %s\\n\" \"$AWS_S3_ADDRESSING_STYLE\" > \"$AWS_CONFIG_FILE\"; python manage.py wait_for_db; python manage.py wait_for_migrations; exec celery -A plane beat -l info'",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "makeplane/plane-backend:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Not served — this container runs the Celery scheduler. Kept so it reads the same configuration as the API.",
                      "defaultValue": "8000"
                    },
                    "DEBUG": {
                      "isOptional": false,
                      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
                      "defaultValue": "0"
                    },
                    "WEB_URL": {
                      "isOptional": false,
                      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AMQP_URL": {
                      "isOptional": false,
                      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
                      "defaultValue": "amqp://plane:${{RabbitMQ.RABBITMQ_DEFAULT_PASS}}@${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}:5672/plane"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Valkey connection string — cache, sessions and the live server's bus.",
                      "defaultValue": "redis://${{Valkey.RAILWAY_PRIVATE_DOMAIN}}:6379/"
                    },
                    "USE_MINIO": {
                      "isOptional": false,
                      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
                      "defaultValue": "0"
                    },
                    "APP_DOMAIN": {
                      "isOptional": false,
                      "description": "Host part of the public address.",
                      "defaultValue": "${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_REGION": {
                      "isOptional": false,
                      "description": "Bucket region.",
                      "defaultValue": "${{Bucket.REGION}}"
                    },
                    "SECRET_KEY": {
                      "isOptional": false,
                      "description": "Must be the same value the API uses — this is the same Django project.",
                      "defaultValue": "${{ Api.SECRET_KEY }}"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "Postgres connection string, wired to the database in this project.",
                      "defaultValue": "postgresql://postgres:${{Postgres.POSTGRES_PASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/plane"
                    },
                    "DB_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "DB_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on before starting Django.",
                      "defaultValue": "5432"
                    },
                    "MQ_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for RabbitMQ before booting.",
                      "defaultValue": "${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "MQ_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for RabbitMQ.",
                      "defaultValue": "5672"
                    },
                    "ENABLE_SIGNUP": {
                      "isOptional": false,
                      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
                      "defaultValue": "1"
                    },
                    "AWS_CONFIG_FILE": {
                      "isOptional": false,
                      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
                      "defaultValue": "/code/aws-config"
                    },
                    "FILE_SIZE_LIMIT": {
                      "isOptional": false,
                      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
                      "defaultValue": "5242880"
                    },
                    "REDIS_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Valkey before booting.",
                      "defaultValue": "${{Valkey.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "REDIS_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for Valkey.",
                      "defaultValue": "6379"
                    },
                    "GUNICORN_WORKERS": {
                      "isOptional": false,
                      "description": "Unused here; this container runs the Celery scheduler.",
                      "defaultValue": "1"
                    },
                    "AWS_ACCESS_KEY_ID": {
                      "isOptional": false,
                      "description": "Bucket access key.",
                      "defaultValue": "${{Bucket.ACCESS_KEY_ID}}"
                    },
                    "MACHINE_SIGNATURE": {
                      "isOptional": false,
                      "description": "Shared with the API.",
                      "defaultValue": "${{ Api.MACHINE_SIGNATURE }}"
                    },
                    "API_KEY_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-key limit on the public REST API.",
                      "defaultValue": "60/minute"
                    },
                    "AWS_S3_BUCKET_NAME": {
                      "isOptional": false,
                      "description": "Bucket holding uploads.",
                      "defaultValue": "${{Bucket.BUCKET}}"
                    },
                    "CELERY_CONCURRENCY": {
                      "isOptional": false,
                      "description": "Unused here; the scheduler queues work rather than running it.",
                      "defaultValue": "2"
                    },
                    "AWS_S3_ENDPOINT_URL": {
                      "isOptional": false,
                      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
                      "defaultValue": "${{Bucket.ENDPOINT}}"
                    },
                    "CORS_ALLOWED_ORIGINS": {
                      "isOptional": false,
                      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_SECRET_ACCESS_KEY": {
                      "isOptional": false,
                      "description": "Bucket secret key.",
                      "defaultValue": "${{Bucket.SECRET_ACCESS_KEY}}"
                    },
                    "ENABLE_EMAIL_PASSWORD": {
                      "isOptional": false,
                      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
                      "defaultValue": "1"
                    },
                    "SIGNED_URL_EXPIRATION": {
                      "isOptional": false,
                      "description": "How long an upload or download link stays valid, in seconds.",
                      "defaultValue": "3600"
                    },
                    "LIVE_SERVER_SECRET_KEY": {
                      "isOptional": false,
                      "description": "Shared with the API and the live server.",
                      "defaultValue": "${{ Api.LIVE_SERVER_SECRET_KEY }}"
                    },
                    "AWS_S3_ADDRESSING_STYLE": {
                      "isOptional": false,
                      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
                      "defaultValue": "path"
                    },
                    "ENABLE_MAGIC_LINK_LOGIN": {
                      "isOptional": false,
                      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
                      "defaultValue": "0"
                    },
                    "AUTHENTICATION_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
                      "defaultValue": "10/minute"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "7cbf0b41-f744-4f2a-86cc-021d32078413": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Live",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": "/live/health",
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "makeplane/plane-live:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to.",
                      "defaultValue": "3000"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Valkey connection, used to fan document updates out between connections.",
                      "defaultValue": "redis://${{Valkey.RAILWAY_PRIVATE_DOMAIN}}:6379/"
                    },
                    "API_BASE_URL": {
                      "isOptional": false,
                      "description": "Private address of the API, which this server calls to authorise each editing session.",
                      "defaultValue": "http://${{Api.RAILWAY_PRIVATE_DOMAIN}}:8000"
                    },
                    "LIVE_BASE_PATH": {
                      "isOptional": false,
                      "description": "Path this server is mounted on. The proxy routes /live/* here and the health check lives at /live/health, so changing it means changing both.",
                      "defaultValue": "/live"
                    },
                    "CORS_ALLOWED_ORIGINS": {
                      "isOptional": false,
                      "description": "Browser origins allowed to open an editing session. This server defaults it to the empty string and then reads that as a one-entry allowlist containing \"\" — which refuses every browser.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "LIVE_SERVER_SECRET_KEY": {
                      "isOptional": false,
                      "description": "Shared secret with the API. If the two differ, opening a page for editing fails to authenticate.",
                      "defaultValue": "${{ Api.LIVE_SERVER_SECRET_KEY }}"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "b4645a21-51aa-4a2b-9eae-4bb04a27bb84": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Admin",
                  "deploy": {
                    "startCommand": "sh -c 'sed -i -e \"s/^worker_processes .*/worker_processes 2;/\" -e \"s|listen 3000;|listen 3000;\\n    listen [::]:3000;\\n    absolute_redirect off;\\n    port_in_redirect off;|\" /etc/nginx/nginx.conf; nginx -t; exec nginx -g \"daemon off;\"'",
                    "healthcheckPath": "/god-mode/",
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 3
                  },
                  "source": {
                    "image": "makeplane/plane-admin:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to.",
                      "defaultValue": "3000"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "ccd27d23-edce-4275-af54-c10e58d2bc4a": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plane.svg",
                  "name": "Api",
                  "deploy": {
                    "startCommand": "bash -c 'set -e; for i in $(seq 1 90); do (echo > /dev/tcp/$DB_WAIT_HOST/$DB_WAIT_PORT) 2>/dev/null && break; echo \"waiting for postgres ($i/90)\"; sleep 2; done; for i in $(seq 1 90); do (echo > /dev/tcp/$REDIS_WAIT_HOST/$REDIS_WAIT_PORT) 2>/dev/null && break; echo \"waiting for valkey ($i/90)\"; sleep 2; done; for i in $(seq 1 90); do (echo > /dev/tcp/$MQ_WAIT_HOST/$MQ_WAIT_PORT) 2>/dev/null && break; echo \"waiting for rabbitmq ($i/90)\"; sleep 2; done; printf \"[default]\\ns3 =\\n    addressing_style = %s\\n\" \"$AWS_S3_ADDRESSING_STYLE\" > \"$AWS_CONFIG_FILE\"; python manage.py wait_for_db; python manage.py migrate; python manage.py register_instance \"$MACHINE_SIGNATURE\"; python manage.py configure_instance; python manage.py create_bucket; python manage.py clear_cache; python manage.py collectstatic --noinput; exec gunicorn -w \"$GUNICORN_WORKERS\" -k uvicorn.workers.UvicornWorker plane.asgi:application --bind \"[::]:$PORT\" --max-requests 1200 --max-requests-jitter 1000 --access-logfile -'",
                    "healthcheckPath": "/",
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "makeplane/plane-backend:v1.4.2"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Railway routes traffic and health checks to. Gunicorn binds [::] on this port, which is dual-stack, so both the health check and the private network are answered.",
                      "defaultValue": "8000"
                    },
                    "DEBUG": {
                      "isOptional": false,
                      "description": "Django debug mode. Leave at 0 — turning it on serves tracebacks and settings to anyone who can reach the API.",
                      "defaultValue": "0"
                    },
                    "WEB_URL": {
                      "isOptional": false,
                      "description": "Public address of this deployment. Used in invitation and notification emails and in links the API generates.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AMQP_URL": {
                      "isOptional": false,
                      "description": "RabbitMQ connection string. This is Celery's broker: every background job passes through it.",
                      "defaultValue": "amqp://plane:${{RabbitMQ.RABBITMQ_DEFAULT_PASS}}@${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}:5672/plane"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Valkey connection string — cache, sessions and the live server's bus.",
                      "defaultValue": "redis://${{Valkey.RAILWAY_PRIVATE_DOMAIN}}:6379/"
                    },
                    "USE_MINIO": {
                      "isOptional": false,
                      "description": "Upstream's flag for running MinIO next door and signing upload URLs against this app's own host. This template uses Railway object storage instead, so the browser talks to the bucket directly — set it to 1 and every upload URL points at a service that does not exist here.",
                      "defaultValue": "0"
                    },
                    "APP_DOMAIN": {
                      "isOptional": false,
                      "description": "Host part of the public address.",
                      "defaultValue": "${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_REGION": {
                      "isOptional": false,
                      "description": "Bucket region.",
                      "defaultValue": "${{Bucket.REGION}}"
                    },
                    "SECRET_KEY": {
                      "isOptional": false,
                      "description": "Django's signing key: sessions, password reset links and the encrypted instance configuration. Changing it logs everyone out and makes the stored admin configuration unreadable. Generated once for this deployment.",
                      "defaultValue": "{{SECRET_KEY}}"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "Postgres connection string, wired to the database in this project.",
                      "defaultValue": "postgresql://postgres:${{Postgres.POSTGRES_PASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/plane"
                    },
                    "DB_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Postgres before running migrations. Railway starts services in parallel, so on a cold deploy the database is usually not up yet.",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "DB_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on before starting Django.",
                      "defaultValue": "5432"
                    },
                    "MQ_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for RabbitMQ before booting.",
                      "defaultValue": "${{RabbitMQ.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "MQ_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for RabbitMQ.",
                      "defaultValue": "5672"
                    },
                    "ENABLE_SIGNUP": {
                      "isOptional": false,
                      "description": "Whether anyone who reaches the URL can register. Read once, on the very first boot, and stored in the database — after that it is changed in the admin UI at /god-mode, not here. Turn it off there once your team has joined.",
                      "defaultValue": "1"
                    },
                    "AWS_CONFIG_FILE": {
                      "isOptional": false,
                      "description": "Where the start command writes the two-line boto3 config below. boto3 takes the addressing style from a file, not from the environment.",
                      "defaultValue": "/code/aws-config"
                    },
                    "FILE_SIZE_LIMIT": {
                      "isOptional": false,
                      "description": "Largest upload accepted, in bytes (5 MB). The proxy enforces the same number, so raise both together.",
                      "defaultValue": "5242880"
                    },
                    "REDIS_WAIT_HOST": {
                      "isOptional": false,
                      "description": "Read by the start command, which waits for Valkey before booting.",
                      "defaultValue": "${{Valkey.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "REDIS_WAIT_PORT": {
                      "isOptional": false,
                      "description": "Port the start command waits on for Valkey.",
                      "defaultValue": "6379"
                    },
                    "GUNICORN_WORKERS": {
                      "isOptional": false,
                      "description": "API worker processes. Two or three suit a busy team; each one costs roughly 150 MB.",
                      "defaultValue": "1"
                    },
                    "AWS_ACCESS_KEY_ID": {
                      "isOptional": false,
                      "description": "Bucket access key.",
                      "defaultValue": "${{Bucket.ACCESS_KEY_ID}}"
                    },
                    "MACHINE_SIGNATURE": {
                      "isOptional": false,
                      "description": "Identifies this installation on first boot. Upstream derives it from the container's hostname, CPU and disk, which on Railway is a new value on every deploy; this is a stable one.",
                      "defaultValue": "{{MACHINE_SIGNATURE}}"
                    },
                    "API_KEY_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-key limit on the public REST API.",
                      "defaultValue": "60/minute"
                    },
                    "AWS_S3_BUCKET_NAME": {
                      "isOptional": false,
                      "description": "Bucket holding uploads.",
                      "defaultValue": "${{Bucket.BUCKET}}"
                    },
                    "CELERY_CONCURRENCY": {
                      "isOptional": false,
                      "description": "Background job workers. Celery's own default is one per CPU the container can see, which on a shared host is a lot of idle processes.",
                      "defaultValue": "2"
                    },
                    "AWS_S3_ENDPOINT_URL": {
                      "isOptional": false,
                      "description": "Object storage endpoint. Attachments, avatars and exports live here.",
                      "defaultValue": "${{Bucket.ENDPOINT}}"
                    },
                    "CORS_ALLOWED_ORIGINS": {
                      "isOptional": false,
                      "description": "Browser origins allowed to call the API, and also the CSRF trusted origins. Leaving it empty does not fail loudly — it switches CORS to allow-all instead.",
                      "defaultValue": "https://${{Proxy.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AWS_SECRET_ACCESS_KEY": {
                      "isOptional": false,
                      "description": "Bucket secret key.",
                      "defaultValue": "${{Bucket.SECRET_ACCESS_KEY}}"
                    },
                    "ENABLE_EMAIL_PASSWORD": {
                      "isOptional": false,
                      "description": "Email and password sign-in. Also first-boot only; the admin UI owns it afterwards.",
                      "defaultValue": "1"
                    },
                    "SIGNED_URL_EXPIRATION": {
                      "isOptional": false,
                      "description": "How long an upload or download link stays valid, in seconds.",
                      "defaultValue": "3600"
                    },
                    "LIVE_SERVER_SECRET_KEY": {
                      "isOptional": false,
                      "description": "Shared secret between the API and the live collaboration server. The two must match or page editing fails to authenticate.",
                      "defaultValue": "{{LIVE_SERVER_SECRET_KEY}}"
                    },
                    "AWS_S3_ADDRESSING_STYLE": {
                      "isOptional": false,
                      "description": "Whether signed URLs are host/bucket/key or bucket.host/key. Railway's storage answers both, but only the path form sends CORS headers — with the other one the browser refuses every upload and every image while curl reports 200.",
                      "defaultValue": "path"
                    },
                    "ENABLE_MAGIC_LINK_LOGIN": {
                      "isOptional": false,
                      "description": "Magic-link sign-in. Off because it needs working SMTP, which is configured in the admin UI at /god-mode.",
                      "defaultValue": "0"
                    },
                    "AUTHENTICATION_RATE_LIMIT": {
                      "isOptional": false,
                      "description": "Per-IP limit on sign-in and sign-up, which is what makes password guessing expensive.",
                      "defaultValue": "10/minute"
                    },
                    "ENABLE_ALPINE_PRIVATE_NETWORKING": {
                      "isOptional": false,
                      "description": "Railway's switch that makes *.railway.internal resolve inside Alpine-based images. Every image in this template is Alpine, and without it the services cannot find each other.",
                      "defaultValue": "true"
                    }
                  }
                },
                "d30fc419-4a1a-404d-a6b4-14f46533ed02": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/valkey.svg",
                  "name": "Valkey",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 2
                  },
                  "source": {
                    "image": "valkey/valkey:9.1.2-alpine"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port Valkey listens on inside the private network.",
                      "defaultValue": "6379"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "Proxy",
      "method": "GET",
      "path": "/healthz",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 11,
    "needs_volume": true
  },
  "generated_at": "2026-09-19T22:14:40.433Z",
  "generator_version": "0.1.0",
  "status": "degraded",
  "validated_at": "2026-09-19T19:13:06.997Z",
  "success_rate_30d": 0.1667,
  "validation": {
    "last_run_id": "run_31f51cb4eb2e4846972d",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": false,
        "detail": "Your workspace has been restricted. Please contact support to resolve this."
      }
    ],
    "typical_ready_seconds": 159,
    "typical_build_seconds": 0,
    "typical_start_seconds": 10,
    "slowest_service": "Worker"
  }
}
