{
  "manifest_version": "1.0.0",
  "template": {
    "id": "40452331-6ce8-4a9c-8bd4-a45c8d2ef24f",
    "slug": "nvidia-auto-ontology-12",
    "name": "NVIDIA Auto Ontology 1.2",
    "description": "Semantic layer over your databases: ask in plain English, get SQL",
    "url": "https://railway.com/deploy/nvidia-auto-ontology-12",
    "upstream": {
      "repo_url": "https://github.com/nick0lay/railway-templates"
    }
  },
  "status": "unvalidated",
  "validated_at": null,
  "success_rate_30d": null,
  "services": [
    {
      "name": "Auto Ontology",
      "source": {
        "repo": "https://github.com/nick0lay/railway-templates"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "API",
      "source": {
        "repo": "https://github.com/nick0lay/railway-templates"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "Postgres",
      "source": {
        "image": "ghcr.io/railwayapp-templates/postgres-ssl:17"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/postgresql/data",
      "http": false
    },
    {
      "name": "Ingestion",
      "source": {
        "repo": "https://github.com/nick0lay/railway-templates"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "MCP",
      "source": {
        "repo": "https://github.com/nick0lay/railway-templates"
      },
      "needs_volume": false,
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "Auto Ontology",
      "description": "Port the web app listens on",
      "secret": false,
      "strategy": "default",
      "default": "3000"
    },
    {
      "key": "APP_URL",
      "service": "Auto Ontology",
      "description": "Public URL of the web app. Change it when you add a custom domain",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "HOSTNAME",
      "service": "Auto Ontology",
      "description": "Listen on IPv6 and IPv4",
      "secret": false,
      "strategy": "default",
      "default": "::"
    },
    {
      "key": "WEB_RUN_SH",
      "service": "Auto Ontology",
      "description": "Start script: sync the web app's schema, then start",
      "secret": false,
      "strategy": "default",
      "default": "set -e\ncd /app/migrate\nn=0\nuntil sh prisma/migrate.sh; do\n  n=$((n+1))\n  if [ \"$n\" -ge 40 ]; then echo \"frontend-setup: schema sync failed\"; exit 1; fi\n  echo \"frontend-setup: schema sync failed, retrying in 5 s\"\n  sleep 5\ndone\ncd /app\necho \"frontend-setup: starting the web app on port ${PORT:-3000}\"\nexec node app/server.js\n"
    },
    {
      "key": "AUTH_SECRET",
      "service": "Auto Ontology",
      "description": "Secret that signs sessions and tokens (generated)",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "POSTGRES_HOST",
      "service": "Auto Ontology",
      "description": "Postgres host (private network)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_PORT",
      "service": "Auto Ontology",
      "description": "Postgres port",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Auto Ontology",
      "description": "Postgres user",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PYTHON_API_URL",
      "service": "Auto Ontology",
      "description": "Auto Ontology API (private network)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_DATABASE",
      "service": "Auto Ontology",
      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Auto Ontology",
      "description": "Postgres password",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "AUTO_ONTOLOGY_ADMIN_EMAIL",
      "service": "Auto Ontology",
      "description": "Email of the admin account. You log in with it; sign-up is disabled and others join by invite",
      "secret": false,
      "strategy": "ask_user"
    },
    {
      "key": "AUTO_ONTOLOGY_ADMIN_PASSWORD",
      "service": "Auto Ontology",
      "description": "Admin password (generated). Read it here after deploy. It is re-applied on every start, so change it here, not in the app",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "AUTO_ONTOLOGY_MCP_PUBLIC_URL",
      "service": "Auto Ontology",
      "description": "Public URL of the MCP server, without /mcp. Access tokens are issued for this address",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "PORT",
      "service": "API",
      "description": "Port the API listens on (fixed by the app)",
      "secret": false,
      "strategy": "default",
      "default": "3001"
    },
    {
      "key": "API_RUN_SH",
      "service": "API",
      "description": "Start script: sample data, migrations, then the API on IPv6 and IPv4",
      "secret": false,
      "strategy": "default",
      "default": "set -e\npython - <<'PY'\n# api-setup: wait for Postgres, then load the Chinook sample database once.\nimport hashlib, os, sys, time, urllib.request\nimport psycopg\nfrom psycopg import sql\n\nh, p = os.environ[\"POSTGRES_HOST\"], os.environ.get(\"POSTGRES_PORT\", \"5432\")\nu, pw = os.environ[\"POSTGRES_USER\"], os.environ[\"POSTGRES_PASSWORD\"]\ndb = os.environ[\"POSTGRES_DATABASE\"]\n\ndef connect(name):\n    return psycopg.connect(host=h, port=p, user=u, password=pw, dbname=name,\n                           autocommit=True, connect_timeout=5)\n\nfor attempt in range(100):\n    try:\n        connect(db).close()\n        break\n    except Exception as e:\n        print(f\"api-setup: waiting for Postgres ({e.__class__.__name__})\", flush=True)\n        time.sleep(3)\nelse:\n    sys.exit(\"api-setup: Postgres did not become reachable\")\n\nif os.environ.get(\"SAMPLE_DATA\", \"true\").lower() != \"true\":\n    print(\"api-setup: SAMPLE_DATA is not true, skipping the sample database\", flush=True)\n    sys.exit(0)\n\nname = os.environ.get(\"SAMPLE_DB_NAME\", \"chinook\")\nreader = os.environ.get(\"SAMPLE_DB_USER\", \"chinook_reader\")\nreader_pw = os.environ[\"SAMPLE_DB_PASSWORD\"]\nadmin = connect(\"postgres\")\nif not admin.execute(\"SELECT 1 FROM pg_database WHERE datname = %s\", (name,)).fetchone():\n    url = os.environ[\"SAMPLE_DB_SQL_URL\"]\n    print(f\"api-setup: loading sample database {name} from {url}\", flush=True)\n    body = urllib.request.urlopen(url, timeout=120).read()\n    want = os.environ.get(\"SAMPLE_DB_SQL_SHA256\", \"\")\n    if want and hashlib.sha256(body).hexdigest() != want:\n        sys.exit(\"api-setup: sample SQL checksum mismatch\")\n    text = body.decode(\"utf-8\")\n    marker = \"\\\\c chinook;\"\n    if marker in text:\n        text = text.split(marker, 1)[1]\n    admin.execute(sql.SQL(\"CREATE DATABASE {}\").format(sql.Identifier(name + \"_loading\")))\n    with connect(name + \"_loading\") as c:\n        c.execute(text)\n    admin.execute(sql.SQL(\"ALTER DATABASE {} RENAME TO {}\").format(\n        sql.Identifier(name + \"_loading\"), sql.Identifier(name)))\n    print(f\"api-setup: sample database {name} loaded\", flush=True)\nif not admin.execute(\"SELECT 1 FROM pg_roles WHERE rolname = %s\", (reader,)).fetchone():\n    admin.execute(sql.SQL(\"CREATE ROLE {} LOGIN\").format(sql.Identifier(reader)))\nadmin.execute(sql.SQL(\"ALTER ROLE {} WITH LOGIN PASSWORD {}\").format(\n    sql.Identifier(reader), sql.Literal(reader_pw)))\nadmin.execute(sql.SQL(\"GRANT CONNECT ON DATABASE {} TO {}\").format(\n    sql.Identifier(name), sql.Identifier(reader)))\nwith connect(name) as c:\n    c.execute(sql.SQL(\"GRANT USAGE ON SCHEMA public TO {}\").format(sql.Identifier(reader)))\n    c.execute(sql.SQL(\"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {}\").format(sql.Identifier(reader)))\nprint(f\"api-setup: read-only user {reader} ready on {name}\", flush=True)\nPY\necho \"api-setup: running database migrations\"\nalembic upgrade head\necho \"api-setup: starting the API on port 3001 (IPv6 and IPv4)\"\nexec python -c \"\nimport runpy, socket, uvicorn\ndef dual(app, **k):\n    port = int(k.pop('port', 3001)); k.pop('host', None)\n    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)\n    s.bind(('::', port))\n    uvicorn.Server(uvicorn.Config(app, **k)).run(sockets=[s])\nuvicorn.run = dual\nrunpy.run_module('auto_ontology.server', run_name='__main__')\n\"\n"
    },
    {
      "key": "EMBED_MODEL",
      "service": "API",
      "description": "Embedding model. Ingestion and questions must use the same one: changing it later means re-ingesting",
      "secret": false,
      "strategy": "default",
      "default": "nvidia/nemotron-3.5-embed-1b"
    },
    {
      "key": "SAMPLE_DATA",
      "service": "API",
      "description": "Load the Chinook sample database (music store) into Postgres on first start, so there is something to ask about right away",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "RERANK_MODEL",
      "service": "API",
      "description": "Rerank model",
      "secret": false,
      "strategy": "default",
      "default": "nvidia/nemotron-3.5-rerank-8b"
    },
    {
      "key": "POSTGRES_HOST",
      "service": "API",
      "description": "Postgres host (private network)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_PORT",
      "service": "API",
      "description": "Postgres port",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "POSTGRES_USER",
      "service": "API",
      "description": "Postgres user",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "EMBED_ENDPOINT",
      "service": "API",
      "description": "OpenAI-compatible embeddings endpoint",
      "secret": false,
      "strategy": "default",
      "default": "https://integrate.api.nvidia.com/v1"
    },
    {
      "key": "SAMPLE_DB_NAME",
      "service": "API",
      "description": "Name of the sample database",
      "secret": false,
      "strategy": "default",
      "default": "chinook"
    },
    {
      "key": "SAMPLE_DB_USER",
      "service": "API",
      "description": "Read-only user Auto Ontology uses for the sample database",
      "secret": false,
      "strategy": "default",
      "default": "chinook_reader"
    },
    {
      "key": "RERANK_ENDPOINT",
      "service": "API",
      "description": "Rerank endpoint. URLs ending in /reranking or /ranking use NVIDIA NIM's format, URLs ending in /rerank the Cohere/Jina-style format",
      "secret": false,
      "strategy": "default",
      "default": "https://ai.api.nvidia.com/v1/retrieval/nvidia/nemotron-3.5-rerank-8b/reranking"
    },
    {
      "key": "LLM_MAX_INFLIGHT",
      "service": "API",
      "description": "Max concurrent model requests (hosted endpoints return 503 above their limit)",
      "secret": false,
      "strategy": "default",
      "default": "6"
    },
    {
      "key": "POSTGRES_DATABASE",
      "service": "API",
      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "API",
      "description": "Postgres password",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "SAMPLE_DB_SQL_URL",
      "service": "API",
      "description": "Where the sample data comes from (Chinook 1.4.5)",
      "secret": false,
      "strategy": "default",
      "default": "https://github.com/lerocha/chinook-database/releases/download/v1.4.5/Chinook_PostgreSql.sql"
    },
    {
      "key": "CONNECTION_STRINGS",
      "service": "API",
      "description": "Databases Auto Ontology models and answers questions about, comma-separated (postgresql://, mysql://, snowflake://, clickhouse://, trino:// ...). Defaults to the bundled Chinook sample database. Clear it to manage connections in Settings > Connections instead",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "SAMPLE_DB_PASSWORD",
      "service": "API",
      "description": "Password of the read-only sample user (generated)",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "DEFAULT_MODELS_MODEL",
      "service": "API",
      "description": "Chat model used for reasoning and extraction. Models without an openai/, azure/ or aws/ prefix use NVIDIA's client",
      "secret": false,
      "strategy": "default",
      "default": "nvidia/nemotron-3.5-lightning-30b-a3b"
    },
    {
      "key": "SAMPLE_DB_SQL_SHA256",
      "service": "API",
      "description": "Checksum of the sample SQL file",
      "secret": false,
      "strategy": "default",
      "default": "e3fde5c1a5b51a2a91429a702c9ca6e69ba56e6c7f5e112724d70c3d03db695e"
    },
    {
      "key": "INGESTION_SERVICE_URL",
      "service": "API",
      "description": "Ingestion service (private network)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "DEFAULT_MODELS_API_KEY",
      "service": "API",
      "description": "API key for the model endpoints. The defaults use NVIDIA's hosted models: get a free nvapi- key at https://build.nvidia.com. Used for every model (chat, embeddings, rerank) unless you override it",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "DEFAULT_MODELS_ENDPOINT",
      "service": "API",
      "description": "OpenAI-compatible endpoint for the chat models",
      "secret": false,
      "strategy": "default",
      "default": "https://integrate.api.nvidia.com/v1"
    },
    {
      "key": "PGDATA",
      "service": "Postgres",
      "description": "Data directory inside the volume",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/postgresql/data/pgdata"
    },
    {
      "key": "POSTGRES_DB",
      "service": "Postgres",
      "description": "Database for Auto Ontology",
      "secret": false,
      "strategy": "default",
      "default": "auto_ontology"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Postgres",
      "description": "Database superuser",
      "secret": false,
      "strategy": "default",
      "default": "postgres"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Postgres",
      "description": "Database password (generated)",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Postgres",
      "description": "Time Postgres gets to shut down cleanly",
      "secret": false,
      "strategy": "default",
      "default": "60"
    },
    {
      "key": "PORT",
      "service": "Ingestion",
      "description": "Port the ingestion service listens on",
      "secret": false,
      "strategy": "default",
      "default": "3002"
    },
    {
      "key": "EMBED_MODEL",
      "service": "Ingestion",
      "description": "Embedding model. Ingestion and questions must use the same one: changing it later means re-ingesting (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "RERANK_MODEL",
      "service": "Ingestion",
      "description": "Rerank model (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_HOST",
      "service": "Ingestion",
      "description": "Postgres host (private network)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_PORT",
      "service": "Ingestion",
      "description": "Postgres port",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Ingestion",
      "description": "Postgres user",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "API_HEALTH_URL",
      "service": "Ingestion",
      "description": "The ingestion service waits for this before starting, so migrations are done",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "EMBED_ENDPOINT",
      "service": "Ingestion",
      "description": "OpenAI-compatible embeddings endpoint (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "INGESTION_PORT",
      "service": "Ingestion",
      "description": "Port the ingestion service listens on",
      "secret": false,
      "strategy": "default",
      "default": "3002"
    },
    {
      "key": "RERANK_ENDPOINT",
      "service": "Ingestion",
      "description": "Rerank endpoint. URLs ending in /reranking or /ranking use NVIDIA NIM's format, URLs ending in /rerank the Cohere/Jina-style format (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "INGESTION_RUN_SH",
      "service": "Ingestion",
      "description": "Start script: wait for the API, then start on IPv6 and IPv4",
      "secret": false,
      "strategy": "default",
      "default": "set -e\necho \"ingestion-setup: waiting for the API (it runs the migrations)\"\npython - <<'PY'\nimport os, sys, time, urllib.request\nurl = os.environ[\"API_HEALTH_URL\"]\nfor attempt in range(200):\n    try:\n        if urllib.request.urlopen(url, timeout=5).status == 200:\n            sys.exit(0)\n    except Exception as e:\n        print(f\"ingestion-setup: API not ready ({e})\", flush=True)\n    time.sleep(5)\nsys.exit(\"ingestion-setup: API did not become ready\")\nPY\necho \"ingestion-setup: starting the ingestion service on port ${INGESTION_PORT:-3002} (IPv6 and IPv4)\"\nexec python -c \"\nimport runpy, socket, uvicorn\ndef dual(app, **k):\n    port = int(k.pop('port', 3001)); k.pop('host', None)\n    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)\n    s.bind(('::', port))\n    uvicorn.Server(uvicorn.Config(app, **k)).run(sockets=[s])\nuvicorn.run = dual\nrunpy.run_module('auto_ontology.ingestion_service', run_name='__main__')\n\"\n"
    },
    {
      "key": "LLM_MAX_INFLIGHT",
      "service": "Ingestion",
      "description": "Max concurrent model requests (hosted endpoints return 503 above their limit) (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_DATABASE",
      "service": "Ingestion",
      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Ingestion",
      "description": "Postgres password",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CONNECTION_STRINGS",
      "service": "Ingestion",
      "description": "Databases Auto Ontology models and answers questions about, comma-separated (postgresql://, mysql://, snowflake://, clickhouse://, trino:// ...). Defaults to the bundled Chinook sample database. Clear it to manage connections in Settings > Connections instead (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DEFAULT_MODELS_MODEL",
      "service": "Ingestion",
      "description": "Chat model used for reasoning and extraction. Models without an openai/, azure/ or aws/ prefix use NVIDIA's client (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "INGEST_INTERVAL_HOURS",
      "service": "Ingestion",
      "description": "Hours between automatic ingestion runs (one also runs at start)",
      "secret": false,
      "strategy": "default",
      "default": "24"
    },
    {
      "key": "DEFAULT_MODELS_API_KEY",
      "service": "Ingestion",
      "description": "API key for the model endpoints. The defaults use NVIDIA's hosted models: get a free nvapi- key at https://build.nvidia.com. Used for every model (chat, embeddings, rerank) unless you override it (from the API service)",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "DEFAULT_MODELS_ENDPOINT",
      "service": "Ingestion",
      "description": "OpenAI-compatible endpoint for the chat models (from the API service)",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PORT",
      "service": "MCP",
      "description": "Port the MCP server listens on",
      "secret": false,
      "strategy": "default",
      "default": "3003"
    },
    {
      "key": "AUTO_ONTOLOGY_API_URL",
      "service": "MCP",
      "description": "Public URL of the Auto Ontology web app; MCP clients sign in there",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "AUTO_ONTOLOGY_MCP_PORT",
      "service": "MCP",
      "description": "Port the MCP server listens on",
      "secret": false,
      "strategy": "default",
      "default": "3003"
    },
    {
      "key": "AUTO_ONTOLOGY_MCP_PUBLIC_URL",
      "service": "MCP",
      "description": "Public URL of this MCP server, without /mcp. Must match the web app's AUTO_ONTOLOGY_MCP_PUBLIC_URL",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "nvidia-auto-ontology-12"
      }
    },
    "cli": "railway deploy --template nvidia-auto-ontology-12",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "40452331-6ce8-4a9c-8bd4-a45c8d2ef24f",
            "serializedConfig": {
              "services": {
                "01caa6e6-5576-44f4-8685-d4d1e3dd2301": {
                  "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nvidia.svg",
                  "name": "Auto Ontology",
                  "deploy": {
                    "startCommand": "/bin/sh -c \"printenv WEB_RUN_SH > /tmp/run.sh && exec sh /tmp/run.sh\"",
                    "healthcheckPath": "/api/health",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 600,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "nick0lay/railway-templates",
                    "branch": null,
                    "rootDirectory": "/solutions/auto-ontology/web"
                  },
                  "variables": {
                    "PORT": {
                      "description": "Port the web app listens on",
                      "defaultValue": "3000"
                    },
                    "APP_URL": {
                      "description": "Public URL of the web app. Change it when you add a custom domain",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "HOSTNAME": {
                      "description": "Listen on IPv6 and IPv4",
                      "defaultValue": "::"
                    },
                    "WEB_RUN_SH": {
                      "description": "Start script: sync the web app's schema, then start",
                      "defaultValue": "set -e\ncd /app/migrate\nn=0\nuntil sh prisma/migrate.sh; do\n  n=$((n+1))\n  if [ \"$n\" -ge 40 ]; then echo \"frontend-setup: schema sync failed\"; exit 1; fi\n  echo \"frontend-setup: schema sync failed, retrying in 5 s\"\n  sleep 5\ndone\ncd /app\necho \"frontend-setup: starting the web app on port ${PORT:-3000}\"\nexec node app/server.js\n"
                    },
                    "AUTH_SECRET": {
                      "description": "Secret that signs sessions and tokens (generated)",
                      "defaultValue": "{{AUTH_SECRET}}"
                    },
                    "POSTGRES_HOST": {
                      "description": "Postgres host (private network)",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "POSTGRES_PORT": {
                      "description": "Postgres port",
                      "defaultValue": "5432"
                    },
                    "POSTGRES_USER": {
                      "description": "Postgres user",
                      "defaultValue": "${{Postgres.POSTGRES_USER}}"
                    },
                    "PYTHON_API_URL": {
                      "description": "Auto Ontology API (private network)",
                      "defaultValue": "http://${{API.RAILWAY_PRIVATE_DOMAIN}}:3001"
                    },
                    "POSTGRES_DATABASE": {
                      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
                      "defaultValue": "${{Postgres.POSTGRES_DB}}"
                    },
                    "POSTGRES_PASSWORD": {
                      "description": "Postgres password",
                      "defaultValue": "${{Postgres.POSTGRES_PASSWORD}}"
                    },
                    "AUTO_ONTOLOGY_ADMIN_EMAIL": {
                      "description": "Email of the admin account. You log in with it; sign-up is disabled and others join by invite",
                      "defaultValue": "{{AUTO_ONTOLOGY_ADMIN_EMAIL}}"
                    },
                    "AUTO_ONTOLOGY_ADMIN_PASSWORD": {
                      "description": "Admin password (generated). Read it here after deploy. It is re-applied on every start, so change it here, not in the app",
                      "defaultValue": "{{AUTO_ONTOLOGY_ADMIN_PASSWORD}}"
                    },
                    "AUTO_ONTOLOGY_MCP_PUBLIC_URL": {
                      "description": "Public URL of the MCP server, without /mcp. Access tokens are issued for this address",
                      "defaultValue": "https://${{MCP.RAILWAY_PUBLIC_DOMAIN}}"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>": {}
                    }
                  }
                },
                "2ebff617-6b31-40e0-9bd0-53cea0b316fb": {
                  "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nvidia.svg",
                  "name": "API",
                  "deploy": {
                    "startCommand": "/bin/sh -c \"printenv API_RUN_SH > /tmp/run.sh && exec sh /tmp/run.sh\"",
                    "healthcheckPath": "/api/health/live",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 600,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "nick0lay/railway-templates",
                    "branch": null,
                    "rootDirectory": "/solutions/auto-ontology/api"
                  },
                  "variables": {
                    "PORT": {
                      "description": "Port the API listens on (fixed by the app)",
                      "defaultValue": "3001"
                    },
                    "API_RUN_SH": {
                      "description": "Start script: sample data, migrations, then the API on IPv6 and IPv4",
                      "defaultValue": "set -e\npython - <<'PY'\n# api-setup: wait for Postgres, then load the Chinook sample database once.\nimport hashlib, os, sys, time, urllib.request\nimport psycopg\nfrom psycopg import sql\n\nh, p = os.environ[\"POSTGRES_HOST\"], os.environ.get(\"POSTGRES_PORT\", \"5432\")\nu, pw = os.environ[\"POSTGRES_USER\"], os.environ[\"POSTGRES_PASSWORD\"]\ndb = os.environ[\"POSTGRES_DATABASE\"]\n\ndef connect(name):\n    return psycopg.connect(host=h, port=p, user=u, password=pw, dbname=name,\n                           autocommit=True, connect_timeout=5)\n\nfor attempt in range(100):\n    try:\n        connect(db).close()\n        break\n    except Exception as e:\n        print(f\"api-setup: waiting for Postgres ({e.__class__.__name__})\", flush=True)\n        time.sleep(3)\nelse:\n    sys.exit(\"api-setup: Postgres did not become reachable\")\n\nif os.environ.get(\"SAMPLE_DATA\", \"true\").lower() != \"true\":\n    print(\"api-setup: SAMPLE_DATA is not true, skipping the sample database\", flush=True)\n    sys.exit(0)\n\nname = os.environ.get(\"SAMPLE_DB_NAME\", \"chinook\")\nreader = os.environ.get(\"SAMPLE_DB_USER\", \"chinook_reader\")\nreader_pw = os.environ[\"SAMPLE_DB_PASSWORD\"]\nadmin = connect(\"postgres\")\nif not admin.execute(\"SELECT 1 FROM pg_database WHERE datname = %s\", (name,)).fetchone():\n    url = os.environ[\"SAMPLE_DB_SQL_URL\"]\n    print(f\"api-setup: loading sample database {name} from {url}\", flush=True)\n    body = urllib.request.urlopen(url, timeout=120).read()\n    want = os.environ.get(\"SAMPLE_DB_SQL_SHA256\", \"\")\n    if want and hashlib.sha256(body).hexdigest() != want:\n        sys.exit(\"api-setup: sample SQL checksum mismatch\")\n    text = body.decode(\"utf-8\")\n    marker = \"\\\\c chinook;\"\n    if marker in text:\n        text = text.split(marker, 1)[1]\n    admin.execute(sql.SQL(\"CREATE DATABASE {}\").format(sql.Identifier(name + \"_loading\")))\n    with connect(name + \"_loading\") as c:\n        c.execute(text)\n    admin.execute(sql.SQL(\"ALTER DATABASE {} RENAME TO {}\").format(\n        sql.Identifier(name + \"_loading\"), sql.Identifier(name)))\n    print(f\"api-setup: sample database {name} loaded\", flush=True)\nif not admin.execute(\"SELECT 1 FROM pg_roles WHERE rolname = %s\", (reader,)).fetchone():\n    admin.execute(sql.SQL(\"CREATE ROLE {} LOGIN\").format(sql.Identifier(reader)))\nadmin.execute(sql.SQL(\"ALTER ROLE {} WITH LOGIN PASSWORD {}\").format(\n    sql.Identifier(reader), sql.Literal(reader_pw)))\nadmin.execute(sql.SQL(\"GRANT CONNECT ON DATABASE {} TO {}\").format(\n    sql.Identifier(name), sql.Identifier(reader)))\nwith connect(name) as c:\n    c.execute(sql.SQL(\"GRANT USAGE ON SCHEMA public TO {}\").format(sql.Identifier(reader)))\n    c.execute(sql.SQL(\"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {}\").format(sql.Identifier(reader)))\nprint(f\"api-setup: read-only user {reader} ready on {name}\", flush=True)\nPY\necho \"api-setup: running database migrations\"\nalembic upgrade head\necho \"api-setup: starting the API on port 3001 (IPv6 and IPv4)\"\nexec python -c \"\nimport runpy, socket, uvicorn\ndef dual(app, **k):\n    port = int(k.pop('port', 3001)); k.pop('host', None)\n    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)\n    s.bind(('::', port))\n    uvicorn.Server(uvicorn.Config(app, **k)).run(sockets=[s])\nuvicorn.run = dual\nrunpy.run_module('auto_ontology.server', run_name='__main__')\n\"\n"
                    },
                    "EMBED_MODEL": {
                      "description": "Embedding model. Ingestion and questions must use the same one: changing it later means re-ingesting",
                      "defaultValue": "nvidia/nemotron-3.5-embed-1b"
                    },
                    "SAMPLE_DATA": {
                      "description": "Load the Chinook sample database (music store) into Postgres on first start, so there is something to ask about right away",
                      "defaultValue": "true"
                    },
                    "RERANK_MODEL": {
                      "description": "Rerank model",
                      "defaultValue": "nvidia/nemotron-3.5-rerank-8b"
                    },
                    "POSTGRES_HOST": {
                      "description": "Postgres host (private network)",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "POSTGRES_PORT": {
                      "description": "Postgres port",
                      "defaultValue": "5432"
                    },
                    "POSTGRES_USER": {
                      "description": "Postgres user",
                      "defaultValue": "${{Postgres.POSTGRES_USER}}"
                    },
                    "EMBED_ENDPOINT": {
                      "description": "OpenAI-compatible embeddings endpoint",
                      "defaultValue": "https://integrate.api.nvidia.com/v1"
                    },
                    "SAMPLE_DB_NAME": {
                      "description": "Name of the sample database",
                      "defaultValue": "chinook"
                    },
                    "SAMPLE_DB_USER": {
                      "description": "Read-only user Auto Ontology uses for the sample database",
                      "defaultValue": "chinook_reader"
                    },
                    "RERANK_ENDPOINT": {
                      "description": "Rerank endpoint. URLs ending in /reranking or /ranking use NVIDIA NIM's format, URLs ending in /rerank the Cohere/Jina-style format",
                      "defaultValue": "https://ai.api.nvidia.com/v1/retrieval/nvidia/nemotron-3.5-rerank-8b/reranking"
                    },
                    "LLM_MAX_INFLIGHT": {
                      "description": "Max concurrent model requests (hosted endpoints return 503 above their limit)",
                      "defaultValue": "6"
                    },
                    "POSTGRES_DATABASE": {
                      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
                      "defaultValue": "${{Postgres.POSTGRES_DB}}"
                    },
                    "POSTGRES_PASSWORD": {
                      "description": "Postgres password",
                      "defaultValue": "${{Postgres.POSTGRES_PASSWORD}}"
                    },
                    "SAMPLE_DB_SQL_URL": {
                      "description": "Where the sample data comes from (Chinook 1.4.5)",
                      "defaultValue": "https://github.com/lerocha/chinook-database/releases/download/v1.4.5/Chinook_PostgreSql.sql"
                    },
                    "CONNECTION_STRINGS": {
                      "description": "Databases Auto Ontology models and answers questions about, comma-separated (postgresql://, mysql://, snowflake://, clickhouse://, trino:// ...). Defaults to the bundled Chinook sample database. Clear it to manage connections in Settings > Connections instead",
                      "defaultValue": "postgresql://${{SAMPLE_DB_USER}}:${{SAMPLE_DB_PASSWORD}}@${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432/${{SAMPLE_DB_NAME}}"
                    },
                    "SAMPLE_DB_PASSWORD": {
                      "description": "Password of the read-only sample user (generated)",
                      "defaultValue": "{{SAMPLE_DB_PASSWORD}}"
                    },
                    "DEFAULT_MODELS_MODEL": {
                      "description": "Chat model used for reasoning and extraction. Models without an openai/, azure/ or aws/ prefix use NVIDIA's client",
                      "defaultValue": "nvidia/nemotron-3.5-lightning-30b-a3b"
                    },
                    "SAMPLE_DB_SQL_SHA256": {
                      "description": "Checksum of the sample SQL file",
                      "defaultValue": "e3fde5c1a5b51a2a91429a702c9ca6e69ba56e6c7f5e112724d70c3d03db695e"
                    },
                    "INGESTION_SERVICE_URL": {
                      "description": "Ingestion service (private network)",
                      "defaultValue": "http://${{Ingestion.RAILWAY_PRIVATE_DOMAIN}}:3002"
                    },
                    "DEFAULT_MODELS_API_KEY": {
                      "description": "API key for the model endpoints. The defaults use NVIDIA's hosted models: get a free nvapi- key at https://build.nvidia.com. Used for every model (chat, embeddings, rerank) unless you override it",
                      "defaultValue": "{{DEFAULT_MODELS_API_KEY}}"
                    },
                    "DEFAULT_MODELS_ENDPOINT": {
                      "description": "OpenAI-compatible endpoint for the chat models",
                      "defaultValue": "https://integrate.api.nvidia.com/v1"
                    }
                  }
                },
                "45cce1cd-d502-4a32-b2c4-23509e72ef52": {
                  "icon": "https://devicons.railway.app/i/postgresql.svg",
                  "name": "Postgres",
                  "deploy": {
                    "restartPolicyType": "ON_FAILURE",
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "ghcr.io/railwayapp-templates/postgres-ssl:17"
                  },
                  "variables": {
                    "PGDATA": {
                      "description": "Data directory inside the volume",
                      "defaultValue": "/var/lib/postgresql/data/pgdata"
                    },
                    "POSTGRES_DB": {
                      "description": "Database for Auto Ontology",
                      "defaultValue": "auto_ontology"
                    },
                    "POSTGRES_USER": {
                      "description": "Database superuser",
                      "defaultValue": "postgres"
                    },
                    "POSTGRES_PASSWORD": {
                      "description": "Database password (generated)",
                      "defaultValue": "{{POSTGRES_PASSWORD}}"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "description": "Time Postgres gets to shut down cleanly",
                      "defaultValue": "60"
                    }
                  },
                  "volumeMounts": {
                    "b3f422d0-64a9-49be-9d57-b05f50d40ba6": {
                      "mountPath": "/var/lib/postgresql/data"
                    }
                  }
                },
                "b9eb75bd-ddc3-4199-b9f3-d60ae16c6af6": {
                  "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nvidia.svg",
                  "name": "Ingestion",
                  "deploy": {
                    "startCommand": "/bin/sh -c \"printenv INGESTION_RUN_SH > /tmp/run.sh && exec sh /tmp/run.sh\"",
                    "healthcheckPath": "/health/live",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 1200,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "nick0lay/railway-templates",
                    "branch": null,
                    "rootDirectory": "/solutions/auto-ontology/api"
                  },
                  "variables": {
                    "PORT": {
                      "description": "Port the ingestion service listens on",
                      "defaultValue": "3002"
                    },
                    "EMBED_MODEL": {
                      "description": "Embedding model. Ingestion and questions must use the same one: changing it later means re-ingesting (from the API service)",
                      "defaultValue": "${{API.EMBED_MODEL}}"
                    },
                    "RERANK_MODEL": {
                      "description": "Rerank model (from the API service)",
                      "defaultValue": "${{API.RERANK_MODEL}}"
                    },
                    "POSTGRES_HOST": {
                      "description": "Postgres host (private network)",
                      "defaultValue": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "POSTGRES_PORT": {
                      "description": "Postgres port",
                      "defaultValue": "5432"
                    },
                    "POSTGRES_USER": {
                      "description": "Postgres user",
                      "defaultValue": "${{Postgres.POSTGRES_USER}}"
                    },
                    "API_HEALTH_URL": {
                      "description": "The ingestion service waits for this before starting, so migrations are done",
                      "defaultValue": "http://${{API.RAILWAY_PRIVATE_DOMAIN}}:3001/api/health/live"
                    },
                    "EMBED_ENDPOINT": {
                      "description": "OpenAI-compatible embeddings endpoint (from the API service)",
                      "defaultValue": "${{API.EMBED_ENDPOINT}}"
                    },
                    "INGESTION_PORT": {
                      "description": "Port the ingestion service listens on",
                      "defaultValue": "3002"
                    },
                    "RERANK_ENDPOINT": {
                      "description": "Rerank endpoint. URLs ending in /reranking or /ranking use NVIDIA NIM's format, URLs ending in /rerank the Cohere/Jina-style format (from the API service)",
                      "defaultValue": "${{API.RERANK_ENDPOINT}}"
                    },
                    "INGESTION_RUN_SH": {
                      "description": "Start script: wait for the API, then start on IPv6 and IPv4",
                      "defaultValue": "set -e\necho \"ingestion-setup: waiting for the API (it runs the migrations)\"\npython - <<'PY'\nimport os, sys, time, urllib.request\nurl = os.environ[\"API_HEALTH_URL\"]\nfor attempt in range(200):\n    try:\n        if urllib.request.urlopen(url, timeout=5).status == 200:\n            sys.exit(0)\n    except Exception as e:\n        print(f\"ingestion-setup: API not ready ({e})\", flush=True)\n    time.sleep(5)\nsys.exit(\"ingestion-setup: API did not become ready\")\nPY\necho \"ingestion-setup: starting the ingestion service on port ${INGESTION_PORT:-3002} (IPv6 and IPv4)\"\nexec python -c \"\nimport runpy, socket, uvicorn\ndef dual(app, **k):\n    port = int(k.pop('port', 3001)); k.pop('host', None)\n    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)\n    s.bind(('::', port))\n    uvicorn.Server(uvicorn.Config(app, **k)).run(sockets=[s])\nuvicorn.run = dual\nrunpy.run_module('auto_ontology.ingestion_service', run_name='__main__')\n\"\n"
                    },
                    "LLM_MAX_INFLIGHT": {
                      "description": "Max concurrent model requests (hosted endpoints return 503 above their limit) (from the API service)",
                      "defaultValue": "${{API.LLM_MAX_INFLIGHT}}"
                    },
                    "POSTGRES_DATABASE": {
                      "description": "Database holding Auto Ontology's catalog, semantic layer and users",
                      "defaultValue": "${{Postgres.POSTGRES_DB}}"
                    },
                    "POSTGRES_PASSWORD": {
                      "description": "Postgres password",
                      "defaultValue": "${{Postgres.POSTGRES_PASSWORD}}"
                    },
                    "CONNECTION_STRINGS": {
                      "description": "Databases Auto Ontology models and answers questions about, comma-separated (postgresql://, mysql://, snowflake://, clickhouse://, trino:// ...). Defaults to the bundled Chinook sample database. Clear it to manage connections in Settings > Connections instead (from the API service)",
                      "defaultValue": "${{API.CONNECTION_STRINGS}}"
                    },
                    "DEFAULT_MODELS_MODEL": {
                      "description": "Chat model used for reasoning and extraction. Models without an openai/, azure/ or aws/ prefix use NVIDIA's client (from the API service)",
                      "defaultValue": "${{API.DEFAULT_MODELS_MODEL}}"
                    },
                    "INGEST_INTERVAL_HOURS": {
                      "description": "Hours between automatic ingestion runs (one also runs at start)",
                      "defaultValue": "24"
                    },
                    "DEFAULT_MODELS_API_KEY": {
                      "description": "API key for the model endpoints. The defaults use NVIDIA's hosted models: get a free nvapi- key at https://build.nvidia.com. Used for every model (chat, embeddings, rerank) unless you override it (from the API service)",
                      "defaultValue": "${{API.DEFAULT_MODELS_API_KEY}}"
                    },
                    "DEFAULT_MODELS_ENDPOINT": {
                      "description": "OpenAI-compatible endpoint for the chat models (from the API service)",
                      "defaultValue": "${{API.DEFAULT_MODELS_ENDPOINT}}"
                    }
                  }
                },
                "f8d7efa9-14cc-4a12-b861-284517a625c3": {
                  "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nvidia.svg",
                  "name": "MCP",
                  "deploy": {
                    "healthcheckPath": "/.well-known/oauth-protected-resource/mcp",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 300,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "nick0lay/railway-templates",
                    "branch": null,
                    "rootDirectory": "/solutions/auto-ontology/mcp"
                  },
                  "variables": {
                    "PORT": {
                      "description": "Port the MCP server listens on",
                      "defaultValue": "3003"
                    },
                    "AUTO_ONTOLOGY_API_URL": {
                      "description": "Public URL of the Auto Ontology web app; MCP clients sign in there",
                      "defaultValue": "https://${{Auto Ontology.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "AUTO_ONTOLOGY_MCP_PORT": {
                      "description": "Port the MCP server listens on",
                      "defaultValue": "3003"
                    },
                    "AUTO_ONTOLOGY_MCP_PUBLIC_URL": {
                      "description": "Public URL of this MCP server, without /mcp. Must match the web app's AUTO_ONTOLOGY_MCP_PUBLIC_URL",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>": {}
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "Auto Ontology",
      "method": "GET",
      "path": "/api/health",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 5,
    "needs_volume": true
  },
  "generated_at": "2026-11-08T22:15:11.842Z",
  "generator_version": "0.1.0"
}
