SQL Server Read Replica
Firely Server supports routing read traffic to a separate SQL Server read replica, leaving the primary database free to handle writes. This reduces load on the primary instance and can improve query throughput in read-heavy deployments.
Note
Read replica support applies to the resource database only. The Administration database always uses the primary connection.
How traffic is routed
Operation |
Connection used |
Notes |
|---|---|---|
Search and read (GET, search) |
Replica |
CTE-based queries |
Bulk data export / $everything |
Replica |
|
Write operations (POST, PUT, PATCH, DELETE) |
Primary |
|
Conditional reads (If-Match, If-None-Exist) |
Primary |
Run inside a transaction; not affected by replica lag |
PubSub change tracking |
Primary |
Must stay on primary to avoid missing events due to replication lag |
Administration database |
Primary only |
Read replica is never applied to the Administration database |
Configuration
Add ReadReplicaConnectionString to the SqlDbOptions section. When not set, all traffic uses the primary ConnectionString as before.
"SqlDbOptions": {
"ConnectionString": "Server=<primary>;Database=vonk;User Id=<user>;Password=<password>;Encrypt=True",
"ReadReplicaConnectionString": "Server=<replica>;Database=vonk;User Id=<user>;Password=<password>;Encrypt=True",
"SchemaName": "vonk",
"AutoUpdateDatabase": true
}
The replica connection string is validated on startup in the same way as the primary.
Infrastructure requirements
Firely Server routes reads to the replica endpoint you configure. It does not manage replication itself. The replica receives all changes — both data and schema — from the primary through the replication mechanism. This means schema creation and migrations only need to run against the primary; they propagate to the replica automatically.
Two common approaches:
SQL Server Always On Availability Group (AG)
Configure a secondary replica with SECONDARY_ROLE (ALLOW_CONNECTIONS = ALL) and SEEDING_MODE = AUTOMATIC. On first setup, SQL Server seeds the entire database (schema and data) from the primary to the secondary automatically. After that, every write to the primary — including schema migrations — is replicated to the secondary via the AG transaction log stream. Connect Firely Server to the secondary’s listener or direct endpoint as the ReadReplicaConnectionString.
The database must exist on the primary before it can be added to the AG. In practice this means starting Firely Server first so that AutoUpdateDatabase creates the database and schema, and only then configuring the AG to include it.
Note
Adding an existing database to a new AG requires a full backup followed by a restore to the secondary before the secondary can join. Automatic seeding only works for databases that have not yet been replicated. If you are setting up an AG against a pre-existing, populated database, take a full backup on the primary, restore it with NORECOVERY on the secondary, and then add it to the AG.
Azure SQL geo-replica
Create a geo-replica from the Azure portal or CLI. The replica is initialised as a full copy of the primary at that point in time, including schema. All subsequent changes to the primary — data writes and schema migrations — are continuously replicated. The replica gets a separate server hostname (e.g. myserver-replica.database.windows.net). Use that hostname in ReadReplicaConnectionString.
Azure SQL Hyperscale
Azure SQL Hyperscale has built-in named replicas (high-availability replicas and optional named read-scale replicas). For Hyperscale, the recommended approach is to use the same connection string, with added ApplicationIntent=ReadOnly as a ReadReplicaConnectionString. Azure SQL will route the connection to a read-scale replica automatically.
Replication lag and consistency
Both Always On AG (with ASYNCHRONOUS_COMMIT) and Azure SQL geo-replicas replicate asynchronously. The primary commits and returns success before the replica has applied the write. Under normal conditions the lag is sub-second, but it is never zero.
The following cases are handled safely:
Conditional writes (
If-Match,If-None-Exist) run inside a transaction that uses the primary connection. Their read phase is not affected by replica lag.PubSub change tracking reads from the primary, so it always sees the data it just wrote.
The following case carries a residual risk:
SMART on FHIR scope checks: compartment-filter queries for patient-level scopes are sent to the replica. If a patient resource was created moments before the scope check fires, the replica may not yet have the record. This results in a transient false negative — the request is incorrectly rejected or filtered — which self-corrects once the replica catches up. This window is typically less than a second on co-located infrastructure.
This is an inherent property of asynchronous replication. If strict read-your-writes consistency is required for all operations, do not configure a read replica, or use synchronous commit (see below).
Synchronous commit (Always On AG only)
If both instances are co-located (same machine or same datacenter), you can configure the AG with SYNCHRONOUS_COMMIT. The primary then waits for the replica to confirm every write before returning success, which eliminates replication lag entirely.
Warning
Do not use synchronous commit for cross-region replicas. The added write latency from cross-region round trips (typically 50–200 ms per write) will significantly degrade write throughput.
Synchronous commit is not available for Azure SQL geo-replicas, which are always asynchronous.
Read-only database user
The replica connection does not need write permissions. For least-privilege access, create a dedicated read-only user and use its credentials in ReadReplicaConnectionString.
On a self-hosted SQL Server instance:
CREATE LOGIN vonk_readonly WITH PASSWORD = '<password>';
USE vonk;
CREATE USER vonk_readonly FOR LOGIN vonk_readonly;
ALTER ROLE db_datareader ADD MEMBER vonk_readonly;
On Azure SQL with contained database users (server-level logins are not available):
USE vonk;
CREATE USER vonk_readonly WITH PASSWORD = '<password>';
ALTER ROLE db_datareader ADD MEMBER vonk_readonly;
