Database connections
How Quoin connects to your Postgres read replica — the read-only role it needs, what it checks before saving, how it stores the connection string, and why an IP allow-list of Cloudflare's shared ranges is never isolation.
A metric can read its number directly from your own Postgres instead of from events you send. This page is the complete contract for that connection. It states what Quoin asks of your database, what Quoin refuses, and what Quoin keeps.
Built, behind a per-workspace switch
The connection described here is built. It includes the database card on /connect/outcomes, the SQL field on
/metrics/new and Settings → Outcome source. It also includes the egress guard, the read-only probe, the encrypted
storage, the hourly health check and the query log below. Each workspace gets the database path only when Quoin's
operators turn it on for that workspace. Quoin has not yet connected to a real customer database. Every test runs
against an in-process Postgres.
Use a read-only role on a replica
Point Quoin at a read replica. Do not point Quoin at your primary. Give Quoin a role that can read and cannot do anything else:
CREATE ROLE quoin_reader LOGIN PASSWORD '…';
GRANT CONNECT ON DATABASE app TO quoin_reader;
GRANT USAGE ON SCHEMA public TO quoin_reader;
GRANT SELECT ON orders, subscriptions, users TO quoin_reader;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
ALTER ROLE quoin_reader SET default_transaction_read_only = on;Grant SELECT on the tables your metrics read. Do not grant SELECT on the whole schema. The last line is an extra
safeguard. The privileges of the role are what keep it read-only. Before Postgres 15, every role can create objects
in public unless you revoke it. Quoin refuses a role that can create objects there.
The connection string is a normal Postgres URL, for example
postgres://quoin_reader:password@replica.example.com:5432/app. Quoin connects to Postgres only. Quoin does not
support other databases. The connection string must follow these rules:
- It has one host.
- It has no
host=option and nohostaddr=option. - It leaves out
sslmode, or setssslmodetorequire,verify-caorverify-full.
What Quoin checks before it saves anything
"Test connection" and "Verify and replace" both resolve the host and connect. Then they run a probe. The probe reads only the catalogs of Postgres, inside a read-only transaction that it rolls back. The probe never reads a row of your data and never writes. Quoin refuses to save the credential if the role meets one of these conditions. Quoin shows "This connection can write. Use a read-only credential." and the first reason it found. The conditions are:
- The role is a superuser, or it can create databases, create roles, start replication or bypass row-level security.
- The role is a member of
pg_write_all_data,pg_write_server_files,pg_read_server_files,pg_execute_server_program,pg_signal_backendorpg_maintain. In Postgres 17 and later,pg_maintaincan refresh materialized views, lock, vacuum and reindex every table. - The role can insert into, update, delete from or truncate any table or view outside the system schemas of Postgres. The role can have this privilege directly or through a role that it inherits.
- The role can create objects in any non-system schema, or it can create schemas in the database.
The other predefined roles of Postgres, such as pg_checkpoint, pg_monitor or pg_read_all_data, change no rows.
They do not cause a refusal.
Quoin never stores a refused credential. When you replace a credential, Quoin first runs the same probe on the new credential. If Quoin refuses the new credential, the old credential keeps working and nothing changes.
TLS is required and verified against your host name
Every connection uses TLS. The certificate must verify for the host name in your connection string. This rule applies
for every sslmode value, require included. Quoin refuses a replica when a private certificate authority signed
its certificate. This limit applies now. Upload of your own CA certificate is TBD: a later release.
Where Quoin connects, and what it refuses
Quoin opens the connection itself, over a TCP socket from a Cloudflare Worker. Before every probe, metric read and health check, Quoin resolves your host name. Quoin checks every address that the name returns. Then Quoin connects to the address that it checked. Because of this, Quoin catches a name that starts to point to a new place on the next connection. Quoin does not trust the name from the day you saved it. A name whose public address changes, for example in a failover, keeps working.
Quoin refuses these items, and does not connect:
- An IP address in place of a host name, in any spelling (
127.1,2130706433,0x7f000001,[::1]). - Internal names such as
localhost,.local,.internal,.lanand.home.arpa. - A name that has any address in the IANA special-purpose ranges. These ranges are:
- private ranges (
10/8,172.16/12,192.168/16,fc00::/7) - loopback ranges
- link-local ranges (
169.254/16,fe80::/10) - the carrier-shared range
100.64/10 - documentation and benchmarking ranges
- multicast and reserved space
- IPv4 addresses that are wrapped in IPv6
- private ranges (
- Cloud metadata addresses (
169.254.169.254,fd00:ec2::254). Quoin always refuses these.
The same check guards the URL that a notification webhook posts to.
Allow-listing Cloudflare's shared ranges is not isolation
Verified against Cloudflare's own documentation on 11 October 2026: Hyperdrive "uses the Cloudflare IP address ranges" that are published at cloudflare.com/ips. These ranges are shared by every Hyperdrive configuration and by the other products of Cloudflare. The documentation of Cloudflare also states that outbound connections from a Worker leave from the ranges of Cloudflare. Quoin connects in this way. We have not measured this ourselves. If you add the ranges of Cloudflare to your firewall, the set of clients that can reach your replica becomes "anything on Cloudflare". This set includes the traffic of every Cloudflare customer, not only the traffic of Quoin. This is a coarse filter. It is not isolation, and Quoin does not promise that it is.
The controls that do protect your data are the read-only role on a replica and verified TLS. Quoin does not use a Hyperdrive configuration for each customer. Cloudflare allows 10 configured databases per account on the free plan and 25 on a paid one. A configuration is fixed when the Worker is deployed. Quoin connects through its own sockets instead.
How the connection string is stored
Quoin encrypts the connection string before it writes the string. Each workspace has its own data key. A root key
that is held as a Worker secret encrypts that data key. The stored value is bound to your workspace, so no other
workspace can open it. Quoin decrypts the string only inside the request or health check that is about to connect.
Quoin never logs the string and never sends it back. Settings shows the host, port, database and role, with the
password masked. The host, port, database name and role name are stored unencrypted so Settings can show them; the password
never is. To change the password, replace it. You cannot read it out again.
Rotation of the root key and storage of the root key in a hardware key service are TBD: later work.
Health checks and logs
Once an hour, Quoin runs the probe again against every stored connection. If the probe passes, "Last verified" moves forward. If the probe fails, Quoin records the reason, and Settings → Outcome source shows it. If the probe still fails after 24 hours, Quoin writes one alert line for its operators. Quoin writes this line once for each failing period. It does not write a line on every check.
Each metric query writes one log line. The line has these items:
- The key of the metric.
- The workspace.
- The number of statements that the query took.
- The number of rows that came back.
- The time that the query took.
- The outcome: succeeded, timed out, was refused or failed.
The line never contains your SQL, a unit id, a value or anything from the connection string.
What the save-time checks are, and are not
Quoin also checks the SQL of each metric before it saves the metric. The SQL must follow these rules:
- It is a
selectand nothing else. - It uses exactly the parameters
:unit,:fromand:to. - It is one statement.
- It has no data-changing keyword.
- It has no set operation.
- It calls none of the functions on the list of Quoin. These functions run SQL from a string (
query_to_xml,ts_stat,ts_rewrite,crosstaband related functions), sleep, signal, change a setting, touch files, take locks or reach another server.
These checks catch mistakes early. They are a list of known items. They are not the boundary. The boundary has these parts:
- The read-only role.
- The read-only transaction that every statement runs in.
- The 5-second statement timeout.
- A 6-second limit Quoin keeps on its own side of the connection. This limit closes the connection even if a query has switched the statement timeout off.
- The network checks above.
Not yet
- A Postgres database for each environment. Now, one connection serves every environment of a workspace (
TBD:). - Your own CA certificate, a hardware key service for the root key, and rotation of the root key (
TBD:). - The runtime of Cloudflare also describes a binding that provisions Hyperdrive connections at request time. Quoin does
not use it, because it would move the address check above out of the control of Quoin (
TBD:to evaluate).