Skip to main content

Network Architecture

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, at 127.0.0.1:5000.

  • nginx
    Acts as Quill's reverse proxy and external TLS entry point.
    It listens on port 443 and 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 --user option.
    The container must start as root so it can prepare the data volume and start its services.
    It then runs the network-facing services under the quill account.

  • The quill account has UID and GID 999. 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/GID 999 and 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.*, and public.*) 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.* and a.*) 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.

HostnameRoutes toWhat it servesWho uses it
dashboard.acme.myquill.aiQuill web applicationThe operator dashboard, the Quill API, and /healthzOperators using a browser
api.acme.myquill.aiQuill web applicationThe Quill API and /healthz; no dashboard UIScripts and monitoring tools
public.acme.myquill.aiQuill web applicationEmbedded chat pages, widget assets, and Slack webhooksEnd users and Slack
db.acme.myquill.aiRavenDBDirect RavenDB access over mutual TLSApplications using RavenDB.Client

nginx restricts which web routes each hostname can expose:

  • api.<domain> forwards /api/* and /healthz. Other paths return 404.
  • public.<domain> forwards /apps/{slug}/embed/*, /widget/assets/*, and /webhooks/slack/{token}.
    Other paths return 404.
  • dashboard.<domain> serves the dashboard, /api/*, and /healthz, but returns 404 for 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>:

EndpointPurpose
GET /apps/{slug}/embed/{token}Loads the embedded chat page
POST /apps/{slug}/embed/{token}/chatSends 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:

EndpointWhy it is available before authentication
GET /healthzReports Quill health
POST /api/auth/loginValidates the Dashboard API key and creates a browser session
POST /api/auth/logoutEnds the current browser session
GET /api/auth/statusReports whether the caller is authenticated
GET /api/bootstrap/statusReports 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_KEY to 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>, and public.<domain>.
  • For db.<domain> and a.<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 port 443 while 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 port 443 normally.

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 as tcp://a.<domain>:38888, but the default Quill deployment publishes only port 443, and nginx does not route port 38888.

  • Direct access through db.<domain> therefore uses RavenDB's HTTPS API on port 443.
    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 root so 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 unprivileged quill account. 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 additional a. 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.

In this article