# Custom proxy config
URL: https://synquic.tech/docs/guides/custom-proxy-config.md

Add your own OpenResty directives to one domain, who may do it, what is refused, and how Synq tests the config and rolls it back if the edge rejects it.

import { Step, Steps } from 'fumadocs-ui/components/steps';

Synq writes the OpenResty (nginx) configuration for every domain itself. For the few cases its settings don't
cover, an organization admin can add a handful of raw directives to **one domain**. Synq checks them, tests the
whole edge configuration and puts the previous version back automatically if the edge refuses it, so a typo
can't take your other sites down.

<Callout title="Try the built-in settings first" type="info">
Most needs have a safer switch. Upload size, timeouts, buffering and compression are in the project's
**Routing** card on the **Domains & Routes** tab (**Request limits**); rewrites, redirects and response headers
are there too; redirects, basic auth and IP rules are [route rules](/docs/guides/edge-rules). Setting the same
directive in both places makes the edge test fail with a duplicate directive, and your change is rolled back.
</Callout>

## Who can use it

- **Reading**: anyone who can see the domain can open the dialog and view the rendered config.
- **Editing**: organization **owners and admins**. Members with the **Member** role see "Only organization admins
  can change this. You can still see the rendered config." The API needs the `domain:admin` permission.
- **Availability**: every domain of a project on your servers, including free subdomains on your apps domain
  (they are ordinary domains). It isn't offered for a managed subdomain this server doesn't route ("Custom config
  is not available for this domain.").

## Edit a domain's config

<Steps>

<Step>

### Open the editor

Go to **Domains** in the sidebar, open the domain's **⋯** menu and choose **Custom config**. The dialog is titled
"Custom config for \{hostname\}".

</Step>

<Step>

### Write your directives

There are two boxes:

| Box | Where the lines go |
| --- | --- |
| **Server block** | Into this domain's HTTP (port 80) and HTTPS (port 443) `server` blocks, after Synq's own settings. Extra `location` blocks go here. |
| **location / block** | At the end of the domain's `location /`, after the upstream and its proxy headers. |

A byte counter under each box shows "\{used\} / \{max\} bytes".

</Step>

<Step>

### Save and apply

Press **Save and apply**. The result appears in the dialog (below).

</Step>

</Steps>

To remove your directives, empty both boxes and save.

## What happens when you save

1. **Validation.** The API checks every line before anything is stored (see [Rules](#rules)). A refused
   snippet is not saved; the error lists each problem with its box and line, for example
   ``server line 2: `include` is not allowed: it loads other files into the edge config``.
2. **Apply.** The config is stored on the domain and the project's live routes are re-applied. The edge writes
   the new vhost file, runs `openresty -t` on the whole configuration and reloads.
3. **Rollback.** If the test or the reload fails, the edge restores the previous vhost file and Synq restores the
   previous stored config. Your draft stays in the boxes so you can fix it.

The dialog reports one of:

| Result | Meaning |
| --- | --- |
| "Applied. The edge accepted the new config and reloaded." | Live now. |
| "Saved. It will be applied when the project deploys." | The project has no live deployment yet; the config goes live with the first deploy. |
| "The edge rejected this config, so the previous config was restored. Its error:" | Followed by nginx's own error message. Nothing changed on the server. |
| "Nothing changed." | The boxes match what's stored. |

Your directives are stored with the domain and rendered into its vhost every time Synq writes it (a deploy, a
certificate renewal, a route change), so they survive redeploys.

Every save that reaches the edge is recorded in the **Audit log** as `domain.custom_config_updated` or, when the
edge refused it, `domain.custom_config_rejected`. The entry records line counts, sizes and directive names, not
the raw text, since a snippet may contain a header value or token.

## Rendered config

**Rendered config (read-only)** shows the domain's vhost file exactly as the edge holds it. Your lines are
highlighted between the markers `# synq-custom-config: begin (server)` (or `(location)`) and
`# synq-custom-config: end`. Press **Refresh** to read it again. Before the domain is verified or the project
has deployed, it says that no vhost exists yet.

## Rules

Raw config can't be proven safe by reading it, so the checks are layered: Synq refuses what must never reach a
shared edge, checks the structure, and then lets `openresty -t` have the final word.

### Size and structure

- At most **8,192 bytes** and **200 lines** per box, and blocks nested at most **4** deep.
- Every statement ends with `;`, and braces must balance. A snippet can't close the block it's placed in.
- No braces inside quoted values or comments, quoted values stay on one line, and no control characters.
- Put `location` blocks in the **Server block** box, not in **location / block**.

### Refused directives

The dialog summarizes the list: "Not allowed: include, load_module, Lua and njs code, init_* and worker_*
settings, env, user, certificate paths, temp and cache paths, logs or root/alias outside this app, and upstreams on
Synq's own ports." In full:

| Refused | Why |
| --- | --- |
| `include`, `load_module` | Load other files or native code into the edge. |
| Anything starting with `lua`, anything containing `_by_lua`, `js_*` (njs), `perl*` | Run code on the edge. |
| `init_*`, `worker_*`, `env`, `user`, `daemon`, `master_process`, `pid`, `lock_file`, `working_directory`, `thread_pool`, `pcre_jit`, `timer_resolution`, `ssl_engine` and similar | Process-level settings of the shared edge. |
| `ssl_certificate*`, `ssl_password_file`, `ssl_trusted_certificate`, `ssl_client_certificate`, `ssl_crl`, `ssl_dhparam`, `ssl_session_ticket_key`, `ssl_stapling_file`, `ssl_conf_command` | Certificates are managed by Synq. |
| `*_temp_path`, `*_cache_path`, `*_store`, `dav_*`, `xslt_*`, `auth_basic_user_file`, `create_full_put_path` | Read or write files outside the route. Use [basic auth rules](/docs/guides/edge-rules#basic-auth) instead of `auth_basic_user_file`. |
| `listen`, `server_name`, `server`, `http`, `upstream`, `events`, `stream`, `mail` | Would change which traffic reaches the server block. |
| `map`, `geo`, `split_clients` | Declare shared state for every route. |

### Directives with restricted arguments

| Directive | Allowed |
| --- | --- |
| `access_log`, `error_log` | Only files under `/var/log/nginx/`, `/var/log/openresty/` or `/usr/local/openresty/nginx/logs/`, or `access_log off`. |
| `root`, `alias` | One absolute path, without variables or `..`, inside the app's own static files. |
| `proxy_pass`, `fastcgi_pass`, `uwsgi_pass`, `scgi_pass`, `grpc_pass`, `memcached_pass` | A literal address (no variables), not a Unix socket, and not the loopback ports Synq itself uses (the API, the dashboard and OpenResty's management port). |

## Examples

Add an HSTS header, so browsers remember to use HTTPS (**Server block**):

```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```

Keep response headers in the **Server block** box. An `add_header` inside `location /` makes nginx ignore every
`add_header` from the server level for that location, including headers Synq sets for you.

Answer `robots.txt` at the edge for a staging site (**Server block**):

```nginx
location = /robots.txt {
    default_type text/plain;
    return 200 "User-agent: *\nDisallow: /\n";
}
```

Hide a header your framework adds and pass an extra one to the app (**location / block**):

```nginx
proxy_hide_header X-Powered-By;
proxy_set_header X-Forwarded-Host $host;
```

## From the API

```bash
curl -H "Authorization: Bearer $SYNQ_TOKEN" \
  "$SYNQ_URL/api/domains/<domainId>/custom-config"

curl -X PUT -H "Authorization: Bearer $SYNQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"server":"add_header X-Frame-Options \"DENY\" always;","location":""}' \
  "$SYNQ_URL/api/domains/<domainId>/custom-config"
```

`GET` returns the stored `server` and `location` text, whether you may edit it, the limits and the rendered
vhost. `PUT` replaces both parts (an omitted field clears that part) and returns the same view plus a `status` of
`applied`, `saved`, `rolled_back` or `unchanged`; a `rolled_back` result carries the edge's error in `message`.

## Related

<Cards>
  <Card title="Domains page" href="/docs/dashboard/domains" description="Where the Custom config action lives." />
  <Card title="Redirects, basic auth and security rules" href="/docs/guides/edge-rules" description="The safer, structured way to protect and redirect paths." />
  <Card title="The edge" href="/docs/guides/the-edge" description="What the edge is and how it serves your domains." />
</Cards>
