{
  "manifest_version": "1.0.0",
  "template": {
    "id": "32ee3606-b817-4968-8142-9ad32a2325be",
    "slug": "affine-selfhost",
    "name": "AFFiNE-latest",
    "description": "Selfhosted sync backend for AFFiNE version 0.23.0+",
    "url": "https://railway.com/deploy/affine-selfhost",
    "upstream": {
      "image": "ghcr.io/toeverything/affine:stable"
    }
  },
  "services": [
    {
      "name": "Redis",
      "source": {
        "image": "redis"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    },
    {
      "name": "Postgres",
      "source": {
        "image": "pgvector/pgvector:pg16"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/postgresql/data",
      "http": false
    },
    {
      "name": "AFFiNE",
      "source": {
        "image": "ghcr.io/toeverything/affine:stable"
      },
      "needs_volume": true,
      "volume_mount_path": "/root/.affine",
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "REDISHOST",
      "service": "Redis",
      "description": "The internal private hostname used by other services within the same Railway project to reach this Redis instance.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDISPORT",
      "service": "Redis",
      "description": "The standard network port for Redis connections.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    },
    {
      "key": "REDISUSER",
      "service": "Redis",
      "description": "The default username for Redis (Redis 6+ supports ACLs/usernames, but \"default\" is standard).",
      "secret": false,
      "strategy": "default",
      "default": "default"
    },
    {
      "key": "REDIS_URL",
      "service": "Redis",
      "description": "The internal connection string used by your apps running inside Railway to cache data.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "REDISPASSWORD",
      "service": "Redis",
      "description": "Sets the Redis environment variable to match the generated secret below.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "REDIS_PASSWORD",
      "service": "Redis",
      "description": "Generates a random, secure 32-character password using Railway's secret generation syntax.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "REDIS_PUBLIC_URL",
      "service": "Redis",
      "description": "The external connection string (via TCP proxy) allowing you to connect to Redis from your local computer or outside tools.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PGDATA",
      "service": "Postgres",
      "description": "The specific subdirectory inside the container where Postgres stores persistent data on disk.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/postgresql/data/pgdata"
    },
    {
      "key": "PGHOST",
      "service": "Postgres",
      "description": "The internal private hostname used by other services within the same Railway project to reach this database.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "PGPORT",
      "service": "Postgres",
      "description": "The standard network port for PostgreSQL connections.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "PGUSER",
      "service": "Postgres",
      "description": "Sets the Postgres client user to match the variable defined below.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PGDATABASE",
      "service": "Postgres",
      "description": "Sets the target database name to match the variable defined below.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PGPASSWORD",
      "service": "Postgres",
      "description": "Sets the Postgres client password to match the generated secret below.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_DB",
      "service": "Postgres",
      "description": "The actual name of the database to be created (hardcoded to \"affine\").",
      "secret": false,
      "strategy": "default",
      "default": "affine"
    },
    {
      "key": "DATABASE_URL",
      "service": "Postgres",
      "description": "The internal connection string used by your apps running inside Railway.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Postgres",
      "description": "The actual username to be created for the database owner (hardcoded to \"affine\").",
      "secret": false,
      "strategy": "default",
      "default": "affine"
    },
    {
      "key": "SSL_CERT_DAYS",
      "service": "Postgres",
      "description": "The validity period (in days) for self-signed SSL certificates, if the container generates them.",
      "secret": false,
      "strategy": "default",
      "default": "820"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Postgres",
      "description": "Generates a random, secure 32-character password using the Railway template syntax.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "DATABASE_PUBLIC_URL",
      "service": "Postgres",
      "description": "The external connection string (using a TCP proxy) allowing you to connect to the DB from your local computer.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Postgres",
      "description": "The grace period (in seconds) Railway waits for active connections to finish before killing the old container during a redeploy.",
      "secret": false,
      "strategy": "default",
      "default": "60"
    },
    {
      "key": "PORT",
      "service": "AFFiNE",
      "description": "Port used for railway HTTP domain, referring to the AFFiNE server port",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DB_DATABASE",
      "service": "AFFiNE",
      "description": "The name of the PostgreSQL database to use (e.g., \"affine\").",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DB_PASSWORD",
      "service": "AFFiNE",
      "description": "The password for the PostgreSQL user.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DB_USERNAME",
      "service": "AFFiNE",
      "description": "The username for the PostgreSQL database owner.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DATABASE_URL",
      "service": "AFFiNE",
      "description": "The full connection string (postgres://user:pass@host:port/db) used by Prisma.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AFFINE_REVISION",
      "service": "AFFiNE",
      "description": "The Docker image tag/version to pull, \"stable\", \"beta\", or \"canary\", etc.",
      "secret": false,
      "strategy": "default",
      "default": "stable"
    },
    {
      "key": "CONFIG_LOCATION",
      "service": "AFFiNE",
      "description": "The file system path where AFFiNE stores its configuration files.",
      "secret": false,
      "strategy": "default",
      "default": "/root/.affine/config"
    },
    {
      "key": "UPLOAD_LOCATION",
      "service": "AFFiNE",
      "description": "The directory where user uploads (images, blobs) will be persisted.",
      "secret": false,
      "strategy": "default",
      "default": "/root/.affine/storage"
    },
    {
      "key": "DB_DATA_LOCATION",
      "service": "AFFiNE",
      "description": "The path on disk where PostgreSQL stores the actual raw database files. This is not relevant on railway since they are deployed as separate service and volumes, but keeping for consistency with offie AFFiNE guide",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "REDIS_SERVER_HOST",
      "service": "AFFiNE",
      "description": "The hostname or IP address of the Redis cache server.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "REDIS_SERVER_PORT",
      "service": "AFFiNE",
      "description": "The network port for Redis (usually 6379).",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AFFINE_SERVER_PORT",
      "service": "AFFiNE",
      "description": "The internal port the AFFiNE Node.js server listens on.",
      "secret": false,
      "strategy": "default",
      "default": "3010"
    },
    {
      "key": "AFFINE_SERVER_HTTPS",
      "service": "AFFiNE",
      "description": "Set to \"true\" if the app handles SSL termination internally; usually \"false\" if behind a reverse proxy (like Nginx) or using railways generated HTTP domains.",
      "secret": false,
      "strategy": "default",
      "default": "false"
    },
    {
      "key": "REDIS_SERVER_PASSWORD",
      "service": "AFFiNE",
      "description": "The password for Redis authentication (leave empty if no auth is set).",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "REDIS_SERVER_USERNAME",
      "service": "AFFiNE",
      "description": "The username for Redis (often \"default\" or empty for older Redis versions).",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AFFINE_SERVER_EXTERNAL_URL",
      "service": "AFFiNE",
      "description": "The base link for invite and other referrence outside of server. Default to railway HTTP public domain.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "affine-selfhost"
      }
    },
    "cli": "railway deploy --template affine-selfhost",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "32ee3606-b817-4968-8142-9ad32a2325be",
            "serializedConfig": {
              "buckets": {},
              "services": {
                "0cd91946-4c34-44ce-b188-5eadacbcaa5c": {
                  "icon": "https://cdn.sanity.io/images/sy1jschh/production/0ce0bfdcfbdbf69662b1116671f97c2dd788b655-157x157.svg",
                  "name": "Redis",
                  "deploy": {
                    "startCommand": "/bin/sh -c \"rm -rf $RAILWAY_VOLUME_MOUNT_PATH/lost+found/ && exec docker-entrypoint.sh redis-server --requirepass $REDIS_PASSWORD --save 60 1 --dir $RAILWAY_VOLUME_MOUNT_PATH\"",
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "redis",
                    "autoUpdates": {
                      "type": "disabled"
                    }
                  },
                  "variables": {
                    "REDISHOST": {
                      "isOptional": false,
                      "description": "The internal private hostname used by other services within the same Railway project to reach this Redis instance.",
                      "defaultValue": "${{RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "REDISPORT": {
                      "isOptional": false,
                      "description": "The standard network port for Redis connections.",
                      "defaultValue": "6379"
                    },
                    "REDISUSER": {
                      "isOptional": false,
                      "description": "The default username for Redis (Redis 6+ supports ACLs/usernames, but \"default\" is standard).",
                      "defaultValue": "default"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "The internal connection string used by your apps running inside Railway to cache data.",
                      "defaultValue": "redis://${{REDISUSER}}:${{REDIS_PASSWORD}}@${{REDISHOST}}:${{REDISPORT}}"
                    },
                    "REDISPASSWORD": {
                      "isOptional": false,
                      "description": "Sets the Redis environment variable to match the generated secret below.",
                      "defaultValue": "${{REDIS_PASSWORD}}"
                    },
                    "REDIS_PASSWORD": {
                      "isOptional": false,
                      "description": "Generates a random, secure 32-character password using Railway's secret generation syntax.",
                      "defaultValue": "{{REDIS_PASSWORD}}"
                    },
                    "REDIS_PUBLIC_URL": {
                      "isOptional": false,
                      "description": "The external connection string (via TCP proxy) allowing you to connect to Redis from your local computer or outside tools.",
                      "defaultValue": "redis://default:${{REDIS_PASSWORD}}@${{RAILWAY_TCP_PROXY_DOMAIN}}:${{RAILWAY_TCP_PROXY_PORT}}"
                    }
                  },
                  "volumeMounts": {
                    "0cd91946-4c34-44ce-b188-5eadacbcaa5c": {
                      "mountPath": "/data"
                    }
                  },
                  "haTemplateCode": "redis-ha",
                  "haConversionConfig": {
                    "edge": {
                      "label": "Reverse Proxies",
                      "options": [
                        1,
                        2
                      ],
                      "nodeLabel": "HAProxy",
                      "description": "HAProxy instances routing writes to the primary",
                      "defaultValue": 2
                    },
                    "replica": {
                      "label": "Redis Replicas",
                      "options": [
                        2,
                        3,
                        4,
                        5
                      ],
                      "nodeLabel": "Redis",
                      "description": "Nodes running Redis + colocated Sentinel",
                      "defaultValue": 2
                    },
                    "internal": null,
                    "description": "This will convert your Redis instance to an HA cluster with Sentinel failover and HAProxy routing to the primary."
                  }
                },
                "81c5a3cb-4338-42f5-9ae4-d6e447ae207d": {
                  "icon": "https://devicons.railway.app/i/postgresql.svg",
                  "name": "Postgres",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null,
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "pgvector/pgvector:pg16"
                  },
                  "variables": {
                    "PGDATA": {
                      "isOptional": false,
                      "description": "The specific subdirectory inside the container where Postgres stores persistent data on disk.",
                      "defaultValue": "/var/lib/postgresql/data/pgdata"
                    },
                    "PGHOST": {
                      "isOptional": false,
                      "description": "The internal private hostname used by other services within the same Railway project to reach this database.",
                      "defaultValue": "${{RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "PGPORT": {
                      "isOptional": false,
                      "description": "The standard network port for PostgreSQL connections.",
                      "defaultValue": "5432"
                    },
                    "PGUSER": {
                      "isOptional": false,
                      "description": "Sets the Postgres client user to match the variable defined below.",
                      "defaultValue": "${{POSTGRES_USER}}"
                    },
                    "PGDATABASE": {
                      "isOptional": false,
                      "description": "Sets the target database name to match the variable defined below.",
                      "defaultValue": "${{POSTGRES_DB}}"
                    },
                    "PGPASSWORD": {
                      "isOptional": false,
                      "description": "Sets the Postgres client password to match the generated secret below.",
                      "defaultValue": "${{POSTGRES_PASSWORD}}"
                    },
                    "POSTGRES_DB": {
                      "isOptional": false,
                      "description": "The actual name of the database to be created (hardcoded to \"affine\").",
                      "defaultValue": "affine"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "The internal connection string used by your apps running inside Railway.",
                      "defaultValue": "postgresql://${{PGUSER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:5432/${{PGDATABASE}}"
                    },
                    "POSTGRES_USER": {
                      "isOptional": false,
                      "description": "The actual username to be created for the database owner (hardcoded to \"affine\").",
                      "defaultValue": "affine"
                    },
                    "SSL_CERT_DAYS": {
                      "isOptional": false,
                      "description": "The validity period (in days) for self-signed SSL certificates, if the container generates them.",
                      "defaultValue": "820"
                    },
                    "POSTGRES_PASSWORD": {
                      "isOptional": false,
                      "description": "Generates a random, secure 32-character password using the Railway template syntax.",
                      "defaultValue": "{{POSTGRES_PASSWORD}}"
                    },
                    "DATABASE_PUBLIC_URL": {
                      "isOptional": false,
                      "description": "The external connection string (using a TCP proxy) allowing you to connect to the DB from your local computer.",
                      "defaultValue": "postgresql://${{PGUSER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_TCP_PROXY_DOMAIN}}:${{RAILWAY_TCP_PROXY_PORT}}/${{PGDATABASE}}"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "isOptional": false,
                      "description": "The grace period (in seconds) Railway waits for active connections to finish before killing the old container during a redeploy.",
                      "defaultValue": "60"
                    }
                  },
                  "volumeMounts": {
                    "81c5a3cb-4338-42f5-9ae4-d6e447ae207d": {
                      "mountPath": "/var/lib/postgresql/data"
                    }
                  }
                },
                "acd0f728-bffe-4668-b77c-5f6333eb3bfb": {
                  "icon": "https://affine.pro/favicon-96.png",
                  "name": "AFFiNE",
                  "deploy": {
                    "startCommand": "",
                    "healthcheckPath": null,
                    "preDeployCommand": [
                      "node ./scripts/self-host-predeploy.js"
                    ],
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "ghcr.io/toeverything/affine:stable"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": false,
                      "description": "Port used for railway HTTP domain, referring to the AFFiNE server port",
                      "defaultValue": "${{AFFiNE.AFFINE_SERVER_PORT}}"
                    },
                    "DB_DATABASE": {
                      "isOptional": false,
                      "description": "The name of the PostgreSQL database to use (e.g., \"affine\").",
                      "defaultValue": "${{Postgres.PGDATABASE}}"
                    },
                    "DB_PASSWORD": {
                      "isOptional": false,
                      "description": "The password for the PostgreSQL user.",
                      "defaultValue": "${{Postgres.PGPASSWORD}}"
                    },
                    "DB_USERNAME": {
                      "isOptional": false,
                      "description": "The username for the PostgreSQL database owner.",
                      "defaultValue": "${{Postgres.PGUSER}}"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "The full connection string (postgres://user:pass@host:port/db) used by Prisma.",
                      "defaultValue": "${{Postgres.DATABASE_URL}}"
                    },
                    "AFFINE_REVISION": {
                      "isOptional": false,
                      "description": "The Docker image tag/version to pull, \"stable\", \"beta\", or \"canary\", etc.",
                      "defaultValue": "stable"
                    },
                    "CONFIG_LOCATION": {
                      "isOptional": false,
                      "description": "The file system path where AFFiNE stores its configuration files.",
                      "defaultValue": "/root/.affine/config"
                    },
                    "UPLOAD_LOCATION": {
                      "isOptional": false,
                      "description": "The directory where user uploads (images, blobs) will be persisted.",
                      "defaultValue": "/root/.affine/storage"
                    },
                    "DB_DATA_LOCATION": {
                      "isOptional": false,
                      "description": "The path on disk where PostgreSQL stores the actual raw database files. This is not relevant on railway since they are deployed as separate service and volumes, but keeping for consistency with offie AFFiNE guide",
                      "defaultValue": "${{Postgres.PGDATA}}"
                    },
                    "REDIS_SERVER_HOST": {
                      "isOptional": false,
                      "description": "The hostname or IP address of the Redis cache server.",
                      "defaultValue": "${{Redis.REDISHOST}}"
                    },
                    "REDIS_SERVER_PORT": {
                      "isOptional": false,
                      "description": "The network port for Redis (usually 6379).",
                      "defaultValue": "${{Redis.REDISPORT}}"
                    },
                    "AFFINE_SERVER_PORT": {
                      "isOptional": false,
                      "description": "The internal port the AFFiNE Node.js server listens on.",
                      "defaultValue": "3010"
                    },
                    "AFFINE_SERVER_HTTPS": {
                      "isOptional": false,
                      "description": "Set to \"true\" if the app handles SSL termination internally; usually \"false\" if behind a reverse proxy (like Nginx) or using railways generated HTTP domains.",
                      "defaultValue": "false"
                    },
                    "REDIS_SERVER_PASSWORD": {
                      "isOptional": false,
                      "description": "The password for Redis authentication (leave empty if no auth is set).",
                      "defaultValue": "${{Redis.REDISPASSWORD}}"
                    },
                    "REDIS_SERVER_USERNAME": {
                      "isOptional": false,
                      "description": "The username for Redis (often \"default\" or empty for older Redis versions).",
                      "defaultValue": "${{Redis.REDISUSER}}"
                    },
                    "AFFINE_SERVER_EXTERNAL_URL": {
                      "description": "The base link for invite and other referrence outside of server. Default to railway HTTP public domain.",
                      "defaultValue": "http://${{RAILWAY_PUBLIC_DOMAIN}}"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>:3010": {
                        "port": 3010
                      }
                    }
                  },
                  "volumeMounts": {
                    "acd0f728-bffe-4668-b77c-5f6333eb3bfb": {
                      "mountPath": "/root/.affine"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {},
  "resources": {
    "expected_services": 3,
    "needs_volume": true
  },
  "generated_at": "2026-09-21T22:14:44.200Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-20T21:17:11.873Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_7634befe025046c9b8d1",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 91,
    "typical_build_seconds": 0,
    "typical_start_seconds": 10,
    "slowest_service": "AFFiNE"
  }
}
