# synq.json
URL: https://synquic.tech/docs/reference/synq-json.md

The declarative deploy config for Synq — like vercel.json or railway.toml. Declare framework, build, runtime, env, domains, routes, resources, services and monorepo layout, and Synq deploys the same way every time.

import { TypeTable } from "fumadocs-ui/components/type-table";

`synq.json` is a repo-root file that tells Synq how to build, run, route, and scale a
project. It mirrors what you'd otherwise set in the [deploy wizard](/docs/guides/deploy-from-github),
so a repo becomes "ready to ship on Synq" and [auto-deploy on push](/docs/guides/auto-deploy)
and `synq deploy` become deterministic — no wizard needed.

<Callout title="Authoritative overlay">
  Synq **auto-detects** your stack first, then applies every field present in `synq.json` on
  top. Fields you omit keep their detected value. So a good `synq.json` is **small** — declare
  only what you want to override or pin, not the whole detected config.
</Callout>

Add the `$schema` line for editor autocomplete and inline validation:

```json
{
  "$schema": "https://synquic.tech/synq.schema.json",
  "framework": "nextjs",
  "port": 3000
}
```

Validate it any time with the CLI:

```bash
synq config init       # scaffold a starter synq.json
synq config validate   # check it against the schema
```

It's **JSON, not JSONC** — no comments, no trailing commas.

## Build

<TypeTable
  type={{
    framework: {
      type: "string (enum)",
      description: "Stack slug — nextjs, vite, django, go, docker, …. Overrides detection.",
    },
    packageManager: {
      type: "string (enum)",
      description:
        "npm, yarn, pnpm, bun, go, cargo, pip, poetry, pipenv, uv, bundler, composer, maven, gradle, dotnet, mix.",
    },
    rootDirectory: {
      type: "string",
      description: 'App directory relative to the repo root (e.g. "./", "apps/web").',
    },
    composePath: {
      type: "string",
      description:
        'Compose file kept outside the detected root — the file ("deploy/stack.yml") or the folder holding it ("deploy/docker-compose"). Makes this a compose deploy.',
    },
    installCommand: { type: "string", description: "Command that installs dependencies." },
    buildCommand: { type: "string", description: "Command that builds the app." },
    startCommand: { type: "string", description: "Command that starts the server in production." },
    outputDirectory: {
      type: "string",
      description: "Directory the build writes to (dist, .next, build, out).",
    },
    buildImage: {
      type: "string",
      description: 'Docker image used for the build (e.g. "node:22").',
    },
    productionPaths: {
      type: "string[]",
      description: "Paths shipped to the runtime as the production artifact.",
    },
  }}
/>

## Runtime

<TypeTable
  type={{
    runtime: {
      type: '"bare" | "docker"',
      description:
        'Runtime isolation for a single app. Services/docker projects are always "docker".',
    },
    productionMode: {
      type: '"host" | "static" | "standalone"',
      description: '"static" ⇒ served as files, no server.',
    },
    port: { type: "integer", description: "Port the server listens on (1–65535)." },
    volumes: {
      type: "string[]",
      description:
        "Paths kept across deploys. Omit to inherit the framework defaults; [] turns persistence off.",
    },
  }}
/>

## Persistent storage

Everything a container writes is discarded when the next version replaces it. `volumes`
names the paths to keep.

A bare path is relative to the app; full compose syntax works too, so you can point a
mount at a specific volume name or a host directory:

```json
{
  "volumes": ["storage", "uploads:/app/public/uploads", "/srv/data:/app/var"]
}
```

Omit the field to inherit the framework's own defaults (a Laravel app keeps `storage/`
with no configuration). Declare `[]` to turn persistence off entirely — that is
different from omitting it.

Named volumes are scoped per project, so two projects that both declare `storage` never
share one. On a bare (non-container) deploy the same paths are kept outside the release
directory and symlinked in.

Compose services declare their own mounts under [`services[].volumes`](#services-compose).

## Environment variables

`env` is an object. A value is a plain string, or `{ "value", "secret" }` to mark a secret
(encrypted at rest — never returned to the client afterward). These declarations are active
automatically; a value saved in the dashboard or CLI for the same key overrides the repository default.

```json
{
  "env": {
    "NEXT_PUBLIC_URL": "https://app.acme.com",
    "DATABASE_URL": { "value": "postgres://…", "secret": true }
  }
}
```

## Domains

`domains` is an array of hostnames or objects. A bare label (`"myapp"`) asks for a free subdomain, which on a
Synq install is `myapp.<your base domain>` (`SYNQ_APPS_BASE_DOMAIN`); a dotted hostname (`"app.acme.com"`) is
a [custom domain](/docs/guides/custom-domains).

```json
{ "domains": ["app.acme.com", { "domain": "api.acme.com", "port": 8080, "type": "custom" }] }
```

<TypeTable
  type={{
    domain: {
      type: "string",
      description: "Hostname. Bare label = free subdomain; dotted = custom.",
    },
    port: { type: "integer", description: "Which port this hostname routes to." },
    targetPath: { type: "string", description: 'Path prefix on the target (default "/").' },
    type: { type: '"free" | "custom"', description: "Overrides the free/custom inference." },
  }}
/>

## Routes

`routes` reproduces `vercel.json`-style routing, compiled to the reverse proxy at deploy.

<TypeTable
  type={{
    rewrites: {
      type: "{ source, destination }[]",
      description: "Internal rewrites (e.g. SPA fallback).",
    },
    redirects: {
      type: "{ source, destination, permanent?, statusCode? }[]",
      description: "3xx redirects.",
    },
    headers: {
      type: "{ source, headers: { key, value }[] }[]",
      description: "Response headers per path.",
    },
    cleanUrls: { type: "boolean", description: "Strip .html extensions." },
    trailingSlash: { type: "boolean", description: "Enforce or remove trailing slashes." },
  }}
/>

## Resources

CPU/memory limits for the project's containers — a named tier, **or** explicit values (which become
the `custom` tier). See [Sleep mode & resources](/docs/guides/sleep-mode-resources).

<Callout title="Self-hosted defaults to no limits">
On a self-hosted instance the default is **`unlimited`** — the machine is the ceiling, because it's
your hardware. Set a tier or explicit values only when you deliberately want to cap a container.
A non-zero value is validated against the **target machine's real capacity**, so a 64 GB box can
hand a container 64 GB.

</Callout>

<TypeTable
  type={{
    tier: {
      type: '"unlimited" | "micro" | "low" | "medium" | "high"',
      description: 'Named sizing tier. "unlimited" = no caps (self-hosted only).',
    },
    cpuCores: { type: "number", description: "Fractional vCPU cores. 0 = no limit." },
    memoryMb: { type: "integer", description: "RAM in MB. 0 = no limit." },
    diskMb: { type: "integer", description: "Disk in MB (hosted-cloud workspaces only; not enforced on Synq). 0 = no limit." },
  }}
/>

```json
{
  "resources": { "cpuCores": 4, "memoryMb": 8192 }
}
```

## Services (compose)

Declaring `services` makes the project a multi-service (Docker) project — see
[Compose / multi-service](/docs/guides/compose-multi-service). Each entry requires a `name`.

Already have a compose file? Don't re-declare it here — point `composePath` at it and leave
`services` out. That works for a compose file anywhere in the repo (`deploy/docker-compose/`),
under any filename (`stack.yml`), and keeps the file the single source of truth: its services are
re-read on every deploy, and `build:` contexts resolve relative to it, exactly as compose does.

```json
{ "composePath": "deploy/docker-compose/docker-compose.yml" }
```

```json
{
  "services": [
    {
      "name": "web",
      "build": ".",
      "buildArgs": { "NEXT_PUBLIC_API_URL": null },
      "ports": ["3000"],
      "exposed": true,
      "domain": "app.acme.com"
    },
    {
      "name": "db",
      "image": "postgres:17",
      "volumes": ["pgdata:/var/lib/postgresql/data"],
      "env": { "POSTGRES_PASSWORD": { "value": "…", "secret": true } },
      "restart": "unless-stopped"
    }
  ]
}
```

<TypeTable
  type={{
    name: { type: "string", description: "Required. Service name." },
    image: { type: "string", description: "Prebuilt image (e.g. postgres:17)." },
    build: { type: "string", description: "Build context path." },
    dockerfile: { type: "string", description: "Dockerfile path." },
    buildArgs: {
      type: "Record<string, string | null>",
      description:
        "Docker build arguments. null inherits the same-named top-level env value; a string overrides it for this service.",
    },
    ports: { type: "string[]", description: 'e.g. ["3000"], ["5432:5432"].' },
    volumes: { type: "string[]", description: 'e.g. ["pgdata:/var/lib/postgresql/data"].' },
    dependsOn: { type: "string[]", description: "Other service names this one depends on." },
    env: { type: "env object", description: "Same shape as top-level env." },
    command: { type: "string", description: "Override the container command." },
    restart: {
      type: '"no" | "always" | "on-failure" | "unless-stopped"',
      description: "Restart policy.",
    },
    exposed: { type: "boolean", description: "Publicly routed." },
    exposedPort: { type: "string", description: "Which container port is exposed." },
    domain: { type: "string", description: "Public hostname for this service." },
    healthcheck: {
      type: "object",
      description: "{ test, interval, timeout, retries, startPeriod, disable }.",
    },
    resources: {
      type: "object",
      description:
        "Per-service cpu/memory caps, overriding the top-level resources field by field. Same shape; 0 = no limit. Parity with compose mem_limit / deploy.resources.limits.",
    },
  }}
/>

`dependsOn` takes service names such as `["db"]`, without a port. It controls container startup
order, not database availability during an image build. For `ENOTFOUND db` during `next build`,
see [Database access during a Next.js build](/docs/guides/compose-multi-service#database-access-during-a-nextjs-build).

Top-level `env` is available to Dockerfile builds as well as running containers. Use
`buildArgs` when a service needs an explicit override or when you want the config to document
which Dockerfile arguments it consumes. A service's own `env` is runtime-only and is never shared
with sibling builds. Keep runtime-only credentials in service `env` where possible: Docker build
arguments are not a secret-storage mechanism and can be retained in image metadata or build cache.

A compose file's own limits are honored too — Synq reads `mem_limit` / `cpus` and
`deploy.resources.limits.{memory,cpus}` off each service and applies them over the project-wide
setting.

## Monorepo

`monorepo` overrides Synq's detected sub-apps. Entries in `apps[]` are matched to detected
sub-apps by `rootDirectory` and override their build settings — they don't declare apps from
scratch (the detector finds the apps).

Each override must have a different normalized `rootDirectory`. Declaring a web
process and a worker at the same root is not a supported multi-app declaration.
`synq config validate` rejects duplicate roots; source scans report unmatched
apps and workspace overrides in the configuration diagnostics.

Create a monorepo through the repository import flow, which supplies the detected
app metadata. The API/SDK also accepts that metadata as `monorepoApps`. Setting
`projectType: "monorepo"` (or CLI `project create --type monorepo`) without it now
fails before creating a project. A local CLI import scans the workspace first.
For independent processes without containers, use separate projects pointed at
the required roots and start commands.

```json
{
  "monorepo": {
    "workspace": { "packageManager": "pnpm", "prepareCommand": "pnpm install && pnpm codegen" },
    "apps": [
      { "name": "web", "rootDirectory": "apps/web", "framework": "nextjs", "port": 3000 },
      { "name": "api", "rootDirectory": "apps/api", "framework": "hono", "port": 8080 }
    ]
  }
}
```

<TypeTable
  type={{
    "workspace.packageManager": { type: "string", description: "Root workspace package manager." },
    "workspace.prepareCommand": {
      type: "string",
      description: "Runs once at the repo root before per-app builds.",
    },
    "apps[].name": { type: "string", description: "Required." },
    "apps[].rootDirectory": {
      type: "string",
      description: "Required. Matches a detected sub-app.",
    },
    "apps[]…": {
      type: "build fields",
      description:
        "framework, packageManager, installCommand, buildCommand, startCommand, outputDirectory, buildImage, port.",
    },
  }}
/>

<Callout title="Not supported yet" type="warn">
  `sleepMode`, monorepo `sharedPaths`, and per-app `domain`/`env`/`exposed` are accepted by the
  validator but **not applied** — leave them out. Set sleep mode and per-app domains in the
  dashboard for now.
</Callout>

## How it maps

| `synq.json`                                                                                                | Deploy wizard / API                               |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `framework`, `packageManager`, `*Command`, `outputDirectory`, `buildImage`, `productionPaths`, `rootDirectory` | Build settings                                    |
| `port`, `productionMode`, `runtime`                                                                            | Runtime settings (`hasServer`, runtime isolation) |
| `volumes`                                                                                                      | Configuration → Persistent storage                |
| `env`                                                                                                          | Environment variables (seeded as editable rows)   |
| `domains`                                                                                                      | Public endpoints / custom domains                 |
| `routes`                                                                                                       | Reverse-proxy routing (`routingConfig`)           |
| `resources`                                                                                                    | Machine Power (resource tier / custom sizing)     |
| `services`                                                                                                     | Compose services                                  |
| `monorepo`                                                                                                     | Monorepo sub-apps + workspace                     |

## See also

- [Guide: deploy with synq.json](/docs/guides/synq-json)
- [Deploy from GitHub](/docs/guides/deploy-from-github) · [Auto-deploy on push](/docs/guides/auto-deploy)
