Network Architecture
-
Quill is deployed as a single, self-contained Docker container.
Its dashboard, API, public chat pages, and RavenDB server are all reached through a single TLS entry point
on port443. -
This article explains what runs inside the Quill container, how incoming connections reach the correct service,
and how each connection is protected. -
In this article:
What runs inside Quill
Quill runs in a single Docker container.
Its three main components are RavenDB, the Quill web application, and nginx.
-
RavenDB
Stores the documents mirrored from your SQL source and the data belonging to each Quill app, including its agents, channels, and conversations. -
The Quill web application
Serves the dashboard, the Quill API, public chat pages, and the Slack event webhook.
It listens only on the container's loopback interface, at127.0.0.1:5000. -
nginx
Acts as Quill's reverse proxy and external TLS entry point.
It listens on port443and uses the requested hostname to route each connection to either the Quill web application or RavenDB.
The process that starts and monitors the container's services runs as root.
The one-time volume-preparation step, privileged setup in individual service scripts, and the long-running certificate-watcher and console-monitor services also run as root.
RavenDB, the Quill web application, nginx, and the four file-logging services run under the dedicated, non-login quill operating-system account.
The certificate watcher drops to quill when regenerating nginx certificate files, and nginx is granted the capability it needs to bind to port 443 without running as root.
Start the container as root
-
Do not override the image's default user with a non-root user by using Docker's
--useroption.
The container must start asrootso it can prepare the data volume and start its services.
It then runs the network-facing services under thequillaccount. -
The
quillaccount has UID and GID999. Named Docker volumes are prepared automatically.
If you bind-mount a host directory at/var/lib/quill, its filesystem must allow the container to assign the managed paths to UID/GID999and allow that identity to write them.
The default Quill deployment publishes only port 443 to the host.
The internal ports used by the Quill web application and RavenDB are not published,
so external connections can only enter through nginx.
How a connection is routed
All external connections enter through nginx on port 443.
At the beginning of a TLS connection, the caller includes the requested hostname in a field called Server Name Indication (SNI). nginx reads this hostname before terminating TLS and uses it to select one of two destination services:
-
The web hostnames (
dashboard.*,api.*, andpublic.*) route to the Quill web application.
nginx terminates TLS using Quill's wildcard server certificate and forwards the request as plain HTTP over the container's loopback interface. -
The database hostnames (
db.*anda.*) route to RavenDB.
nginx passes the encrypted connection through unchanged. RavenDB terminates TLS and validates the caller's client certificate, preserving mutual TLS directly between the caller and RavenDB.
The current nginx configuration matches these hostnames by prefix.
A hostname that does not begin with db., a., public., or api. follows the dashboard route and remains subject to dashboard authentication.
┌────────────────── the container ──────────────────┐
│ │
caller ── TLS :443 ────> │ nginx │
│ ├─ dashboard.* ─┐ │
│ ├─ api.* ─┼─ terminate TLS ─> Quill web │
│ ├─ public.* ─┘ │
│ │ │
│ ├─ db.* ──┐ │
│ └─ a.* ──┴─ TLS passthrough ─> RavenDB │
│ │
│ Quill web ── admin client certificate ─> RavenDB │
└───────────────────────────────────────────────────┘
For the web hostnames, nginx carries the caller's IP address from the SNI front to the TLS-terminating listeners using the PROXY protocol.
Those listeners restore the original address and pass it to the Quill web application in X-Forwarded-For.
For db.<domain> and a.<domain>, an internal stream hop consumes the PROXY header before relaying the encrypted connection to RavenDB.
TLS remains end-to-end between the caller and RavenDB, but RavenDB sees the internal proxy hop rather than the caller's source address.
The login endpoint and public chat endpoint both apply IP-based rate limits.
Because the Quill web application receives the original client address, each limit uses separate per-client buckets.
The Quill hostnames
During sign-up, you choose a Quill name for your Quill instance.
Quill uses this name to create its addresses under the myquill.ai domain; you do not need to own a domain or create the DNS records yourself.
For example, if you choose acme as the Quill name, your instance uses the base domain acme.myquill.ai.
In the examples below, <domain> represents this base domain.
| Hostname | Routes to | What it serves | Who uses it |
|---|---|---|---|
dashboard.acme.myquill.ai | Quill web application | The operator dashboard, the Quill API, and /healthz | Operators using a browser |
api.acme.myquill.ai | Quill web application | The Quill API and /healthz; no dashboard UI | Scripts and monitoring tools |
public.acme.myquill.ai | Quill web application | Embedded chat pages, widget assets, and Slack webhooks | End users and Slack |
db.acme.myquill.ai | RavenDB | Direct RavenDB access over mutual TLS | Applications using RavenDB.Client |
nginx restricts which web routes each hostname can expose:
api.<domain>forwards/api/*and/healthz. Other paths return404.public.<domain>forwards/apps/{slug}/embed/*,/widget/assets/*, and/webhooks/slack/{token}.
Other paths return404.dashboard.<domain>serves the dashboard,/api/*, and/healthz, but returns404for public embed and webhook routes.
The a. routing alias
The nginx configuration also recognizes a.<domain> and passes it to the same RavenDB listener as db.<domain>.
RavenDB uses this address as its advertised node URL.
This is not a separate Quill web surface.
Use db.<domain> when configuring your own applications.
What is public
public.<domain> is Quill's end-user and Slack callback surface.
It does not use the Dashboard API key, a browser session, or a client certificate.
Embedded chat access is granted by the embed-link token included in the URL.
Slack event deliveries use a per-channel token in the callback URL and a Slack request signature.
nginx forwards these four endpoint patterns on public.<domain>:
| Endpoint | Purpose |
|---|---|
GET /apps/{slug}/embed/{token} | Loads the embedded chat page |
POST /apps/{slug}/embed/{token}/chat | Sends a prompt and streams the agent's reply |
GET /widget/assets/{file} | Loads the JavaScript and CSS required by the chat page |
POST /webhooks/slack/{token} | Receives Slack event deliveries |
The embedded chat page loads its widget assets from the same public.<domain> origin.
Any proxy or web application firewall placed in front of Quill must allow this path.
All other paths on public.<domain> return 404.
Quill masks URL-based embed-link and Slack webhook tokens in nginx's access and error logs,
retaining only the first six characters for correlation.
Anyone with an active embed link can use its chat page, subject to the link and channel controls:
- The link belongs to one channel in one Quill app.
- The link expires at its configured time.
- The link allows a configured maximum number of chat invocations.
- An operator can revoke the link, preventing subsequent access.
- The channel can restrict which websites may embed its chat page by listing their allowed origins.
The dashboard leaves this list empty by default, allowing the widget to be embedded on any website until you add one or more origins.
Allowed origins restrict browser embedding and chat requests that identify their origin,
but they do not replace the embed-link token as the access credential.
Chat requests that do not contain an Origin header are allowed.
Quill resolves the app slug first and then looks for the embed-link token only in that app's RavenDB database.
The link identifies a channel and agent within the same app, so using it with another app slug returns 404.
The separate database for each app provides an additional isolation boundary.
A few support endpoints on dashboard.<domain> and api.<domain> are reachable without presenting the Dashboard API key or an authenticated browser session:
| Endpoint | Why it is available before authentication |
|---|---|
GET /healthz | Reports Quill health |
POST /api/auth/login | Validates the Dashboard API key and creates a browser session |
POST /api/auth/logout | Ends the current browser session |
GET /api/auth/status | Reports whether the caller is authenticated |
GET /api/bootstrap/status | Reports activation progress |
The dashboard's HTML and static assets are also available so that the login screen can load.
They do not expose app data or administrative functionality.
All operational API endpoints for apps, agents, channels, connection strings, conversations, certificates, and usage require the Dashboard API key or an authenticated browser session.
What is protected, and by what
Access to Quill's dashboard and operational APIs requires the Dashboard API key or an authenticated browser session.
Direct RavenDB access requires a client certificate that RavenDB recognizes.
Dashboard API key
The Dashboard API key protects Quill's operational API endpoints on dashboard.<domain> and api.<domain>.
Programmatic callers send the key in the X-Api-Key header.
An operator signing in through the dashboard exchanges the key for a browser session cookie.
Protected endpoints return 401 Unauthorized when neither a valid Dashboard API key nor an authenticated browser session is provided.
For example, the following request uses the Dashboard API key to retrieve the list of apps:
curl -H "X-Api-Key: QUILLDASH-..." \
https://api.<domain>/api/apps/
The Quill instance receives the Dashboard API key through the QUILL_API_KEY environment variable.
It persists only a salted hash of the key in its configuration database and uses constant-time comparisons during validation.
The Dashboard API key is separate from the License key supplied through QUILL_LICENSE_KEY,
which Quill uses to retrieve the setup package during activation.
Client certificate
A client certificate protects direct RavenDB access through db.<domain> and the a.<domain> routing alias.
The Dashboard API key cannot authenticate a direct connection to RavenDB.
The caller must present a client certificate that RavenDB recognizes.
You can generate client certificates from the dashboard's Certificates page and assign each certificate the appropriate security clearance and database permissions.
If the Dashboard API key is exposed
-
Quill does not currently provide a built-in command for rotating or revoking the Dashboard API key.
-
To replace the Dashboard API key accepted by this Quill instance, set
QUILL_API_KEYto a new high-entropy value in your deployment configuration and recreate the container.
Quill reads the new value at startup and replaces the salted hash stored in its configuration database. -
This changes only the Dashboard API key accepted by that Quill instance.
It does not rotate the signup-issued Dashboard API key, which can be supplied again to Quill instances registered later under the same email address. -
Repeat the local replacement on every existing Quill instance that uses the exposed Dashboard API key.
If the signup-issued Dashboard API key itself is exposed, contact Quill support.
Internal service ports
RavenDB starts in secured mode after the setup package becomes available and listens on the container's loopback interface at 127.0.0.1:8443.
The Quill web application listens on the loopback interface at 127.0.0.1:5000.
These internal addresses allow the container's services to communicate without accepting connections from the host or peer containers.
For diagnostics, run commands inside the container. In this command, <container-name> is the name specified by docker run --name and, by default, matches your Quill name.
docker exec <container-name> curl -s http://127.0.0.1:5000/api/bootstrap/status
Keep internal ports unpublished
-
Publish only nginx's external entry point on port
443. -
Do not publish RavenDB's or the Quill web application's internal ports;
nginx must remain the only external entry point.
The TLS front and the wildcard certificate
Certificate use
The Quill setup package contains a wildcard server certificate for *.<domain>.
This certificate covers the four customer-facing hostnames and RavenDB's a.<domain> routing alias.
The wildcard server certificate is separate from the client certificates used to authenticate direct RavenDB connections.
You do not install the server certificate manually.
During activation, Quill extracts the setup package and configures both nginx and RavenDB to use the certificate:
- nginx converts the certificate and private key into the required PEM format and presents it for
dashboard.<domain>,api.<domain>, andpublic.<domain>. - For
db.<domain>anda.<domain>, nginx passes TLS through unchanged, so RavenDB presents the certificate itself.
Certificate storage
Quill stores the setup package and generated TLS material beneath /var/lib/quill.
On a newly prepared volume, the volume-preparation step creates the setup, certs, and proxy directories with owner-only access.
Setup-package extraction then creates setup/A, and before RavenDB starts, Quill sets setup, setup/A, and certs to owner-only access.
The copied server PFX and nginx private key are readable and writable only by their owner.
Inside the container, the quill account owns this material; root can also access it.
Renewal and monitoring
RavenDB checks the server certificate once an hour and renews it automatically. By default, renewal begins when 30% of the certificate's validity period remains (about 27 days for a 90-day Let's Encrypt certificate).
Renewal requires outbound HTTPS access to api.ravendb.net and the default Let's Encrypt ACME service at acme-v02.api.letsencrypt.org.
If renewal cannot complete before the certificate expires, clients will reject TLS connections to all of these hostnames.
When RavenDB replaces the certificate file, Quill regenerates the PEM files used by nginx and reloads nginx.
No new setup package or manual certificate installation is required.
Renewal failures are recorded in /var/lib/quill/logs/ravendb.log.
Successful nginx certificate reloads are recorded in /var/lib/quill/logs/certwatch.log.
Inspect these logs from the host:
In the commands below, <container-name> is the name specified by docker run --name.
By default, it matches your Quill name.
docker exec <container-name> tail -n 100 /var/lib/quill/logs/ravendb.log
docker exec <container-name> tail -n 100 /var/lib/quill/logs/certwatch.log
Why port 443 is unavailable before activation
-
nginx waits for the server certificate and does not start until the certificate is available.
A newly started Quill container therefore does not accept connections on port443while activation is still in progress. -
This is expected. Check the activation status from inside the container:
docker exec <container-name> curl -s http://127.0.0.1:5000/api/bootstrap/status -
Once activation reports
Ready, connect through port443normally.
Direct database access
Client certificates for your applications
db.<domain> gives your applications direct access to Quill's RavenDB server.
The connection passes through nginx, but the Quill web application is not in the request path.
nginx forwards the encrypted TLS connection unchanged. RavenDB then:
- presents Quill's wildcard server certificate;
- requires the caller to present a recognized client certificate;
- applies the security clearance and database permissions assigned to that certificate.
Native TCP is not exposed
-
The generated RavenDB settings bind the native TCP listener to
tcp://127.0.0.1:38888,
so it is reachable only from inside the container.
RavenDB advertises this listener astcp://a.<domain>:38888, but the default Quill deployment publishes only port443, and nginx does not route port38888. -
Direct access through
db.<domain>therefore uses RavenDB's HTTPS API on port443.
Features that require RavenDB's native TCP protocol, such as data subscriptions, cannot connect from outside the Quill container.
The hostname and database name select different things:
db.<domain>selects the RavenDB server.- The database name selects the data belonging to a specific Quill app.
A Quill instance runs one RavenDB server with a separate database for each app.
The database name is the app slug shown in the dashboard.
Generate a client certificate from the dashboard's Certificates page before connecting your application.
Quill's internal admin certificate
The setup package also contains an admin client certificate for Quill's own RavenDB access.
During activation, Quill loads this certificate inside the container and configures RavenDB to trust it.
The Quill web application uses the certificate for internal operations such as creating app databases, configuring ongoing tasks, managing certificates, and invoking RavenDB APIs.
The certificate is not forwarded to browsers, chat widgets, or API callers, and Quill does not expose an endpoint for downloading it. Instead, Quill authenticates each web request and performs the corresponding RavenDB operation internally.
Browsers and chat widgets therefore cannot authenticate a direct RavenDB connection.
Applications that require direct access must use their own client certificate with appropriately scoped permissions.
Summary
-
The default Quill deployment publishes one port,
443, with nginx as the external entry point. -
The container must start as
rootso it can prepare the data volume and start its services.
RavenDB, the Quill web application, nginx, and the four file-logging services then run under the unprivilegedquillaccount. The web application and RavenDB listen only on the container's loopback interface. -
Four customer-facing hostnames expose the dashboard, API, public chat, and direct RavenDB surfaces.
The additionala.hostname is RavenDB's advertised node alias. -
The public hostname exposes embed-link chat and widget routes, plus the Slack event webhook.
The chat and Slack routes use URL-based tokens, and Slack also validates the request signature. -
Quill's operational APIs require the Dashboard API key or an authenticated browser session.
-
Direct RavenDB access uses the HTTPS API on port
443, requires a recognized client certificate,
and preserves end-to-end mutual TLS through nginx. -
The default Quill deployment does not expose RavenDB's native TCP listener,
so features such as data subscriptions are unavailable externally. -
One wildcard server certificate covers the four customer-facing hostnames and the
a.alias. -
Quill restricts access to the setup package and TLS private-key material inside the container and masks URL-based capability tokens in nginx logs.
-
Quill's admin client certificate is used only inside the Quill container and is not exposed to callers.