Docs
Skip to content

Tooling

Dedicated databases_

Provision dedicated Appwrite PostgreSQL, MySQL, and MongoDB databases with Terraform, including replicas, backups, branches, poolers, and extensions.

7 min read

Raw

A dedicated database runs on infrastructure reserved for a single project, with its own connection string, compute specification, and lifecycle. The provider exposes each engine as its own set of resources, because Appwrite routes them separately and only some engines have a pooler or extensions.

EngineResource prefix
PostgreSQLappwrite_postgresql_
MySQLappwrite_mysql_
MongoDBappwrite_mongo_

For full generated schemas, see the Terraform Registry: postgresql_database, mysql_database, and mongo_database. The provider repository contains the source and examples.

Resources

Every engine has the same database, backup, and branch resources. Poolers and extensions exist only where the engine supports them.

ResourcePurposePostgreSQLMySQLMongoDB
*_databaseProvision and size a dedicated databaseYesYesYes
*_backup_policySchedule backups on a CRON expressionYesYesYes
*_backup_storageSend backups to a bucket you ownYesYesYes
*_branchBranch a database for previews and CIYesYesYes
*_poolerConfigure the connection poolerYesYesNo
*_extensionInstall an engine extensionYesNoNo

Written out, that is appwrite_postgresql_database, appwrite_mysql_database, and appwrite_mongo_database; appwrite_postgresql_backup_policy, appwrite_mysql_backup_policy, and appwrite_mongo_backup_policy; appwrite_postgresql_backup_storage, appwrite_mysql_backup_storage, and appwrite_mongo_backup_storage; appwrite_postgresql_branch, appwrite_mysql_branch, and appwrite_mongo_branch; appwrite_postgresql_pooler and appwrite_mysql_pooler; and appwrite_postgresql_extension.

Data sources

Data sources read what already exists instead of creating it. Use them to size a database from what your plan allows, to look up a connection string, or to report on live health.

Data sourcePurposePostgreSQLMySQLMongoDB
*_databaseLook up one database by ID, including connection credentialsYesYesYes
*_databasesList databases, with server-side query filteringYesYesYes
*_specificationsList the compute specifications your billing plan allowsYesYesYes
*_database_statusRead live health, replication, connections, and volumesYesYesYes
*_backupsList backups, for example to find an ID to restore fromYesYesYes
*_extensionsList installed and installable extensionsYesNoNo

The *_databases listing omits connection credentials on purpose, so listing every database does not put every password into state. Read a single *_database data source when you need the connection string.

Provisioning a database

At minimum a database needs a name. Set specification to pick the compute size, and version to pin the engine major version.

Terraform
resource "appwrite_postgresql_database" "main" {
name = "main"
version = "17"
specification = "s-1vcpu-1gb"
}

Changing specification resizes the database in place. Changing version performs an in-place major version upgrade, which cannot be rolled back.

Sizing from the specifications data source

Compute slugs (s-1vcpu-1gb through s-8vcpu-64gb) are not all enabled on every billing plan, and Appwrite rejects a slug your plan does not allow at apply time. Read the catalog to see what you can pick:

Terraform
data "appwrite_postgresql_specifications" "available" {}
output "available_specifications" {
value = [
for s in data.appwrite_postgresql_specifications.available.specifications :
{ slug = s.slug, cpu = s.cpu, memory = s.memory, price = s.price }
if s.enabled
]
}

Each specification reports slug, name, cpu, memory, max_connections, included_storage, included_bandwidth, price, and enabled.

Then set the slug you want. How much compute a database gets is worth choosing rather than deriving from whatever the catalog returns first:

Terraform
resource "appwrite_postgresql_database" "production" {
name = "production"
version = "17"
specification = "s-2vcpu-4gb"
}

To catch an unavailable slug at plan time rather than halfway through an apply, check your choice against the catalog:

Terraform
variable "database_specification" {
type = string
default = "s-2vcpu-4gb"
}
resource "appwrite_postgresql_database" "production" {
name = "production"
version = "17"
specification = var.database_specification
lifecycle {
precondition {
condition = contains(
[for s in data.appwrite_postgresql_specifications.available.specifications : s.slug if s.enabled],
var.database_specification
)
error_message = "Specification ${var.database_specification} is not enabled on this billing plan."
}
}
}

High availability and recovery

Setting replicas above 0 turns on high availability. sync_mode applies only when there are replicas. async never blocks a commit, sync waits for a standby, and quorum waits for a majority.

Terraform
resource "appwrite_postgresql_database" "production" {
name = "production"
specification = "s-2vcpu-4gb"
# A warm standby with synchronous replication.
replicas = 1
sync_mode = "sync"
# Point-in-time recovery with a two week window.
pitr = true
pitr_retention_days = 14
# Grow storage automatically, up to 100 GB.
storage_autoscaling = true
storage_autoscaling_threshold_percent = 80
storage_autoscaling_max_gb = 100
}

Set storage_autoscaling_max_gb to 0 for no ceiling.

Networking and maintenance

network_ip_allowlist takes IP addresses and CIDR ranges. An empty set allows any address, so the allowlist restricts nothing until it has an entry. maintenance_window_day and maintenance_window_hour_utc must be set together.

Terraform
resource "appwrite_mysql_database" "production" {
name = "production"
specification = "s-2vcpu-4gb"
network_ip_allowlist = ["203.0.113.0/24", "10.0.0.0/16"]
network_idle_timeout_seconds = 300
# Patch on Sunday mornings rather than mid-week.
maintenance_window_day = "sun"
maintenance_window_hour_utc = 3
}

Pausing and idling

idle_timeout_minutes scales the container to zero after a period of inactivity, and 0 keeps it always on. status is the desired state. Set it to paused to stop a database without deleting its data, and back to ready to resume. When you leave it unset, Terraform only reads status back from the server.

Terraform
resource "appwrite_postgresql_database" "development" {
name = "development"
specification = "s-1vcpu-1gb"
idle_timeout_minutes = 15
status = "ready"
}

The read-only lifecycle_state reports how far an idling database has scaled down: active, warm, cold, or hibernated.

SQL API

The SQL API runs statements over the Appwrite API rather than a direct connection. It is off by default. DDL and DCL statements (CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE) are rejected unless you list them in sql_api_allowed_statements.

Terraform
resource "appwrite_postgresql_database" "analytics" {
name = "analytics"
specification = "s-1vcpu-1gb"
sql_api_enabled = true
sql_api_allowed_statements = ["SELECT"]
sql_api_max_rows = 1000
sql_api_max_bytes = 1048576
sql_api_timeout_seconds = 30
}

MongoDB accepts these arguments so the schema is the same across engines, but it does not run SQL, so they have no effect.

Connection details

Each database exports its connection details as read-only attributes. connection_string and connection_password are sensitive. They land in Terraform state, so protect the state file and mark any output that carries them.

Terraform
output "database_host" {
value = appwrite_postgresql_database.main.hostname
}
output "database_url" {
value = appwrite_postgresql_database.main.connection_string
sensitive = true
}

The database also exports connection_user, connection_port, ssl, cpu, memory, storage, engine, network_max_connections, backup_enabled, created_at, updated_at, and error when the status is failed. Rotate credentials outside Terraform, since the provider only reads them.

Connection pooling

A pooler sits in front of PostgreSQL and MySQL databases. It exists for the lifetime of the database, so this resource only updates settings. Destroying it leaves the pooler running with its last applied configuration.

Terraform
resource "appwrite_postgresql_pooler" "main" {
database_id = appwrite_postgresql_database.production.id
mode = "transaction"
default_pool_size = 25
# Send SELECTs to the replica and keep writes on the primary.
read_write_splitting = true
}

mode is transaction (a connection returns to the pool after each transaction, which suits short serverless queries) or session (held for the whole client session). read_write_splitting is only active when the database has replicas.

max_connections is settable on MySQL and read-only on PostgreSQL. The PostgreSQL pooler has no client cap of its own and reports the database's network_max_connections instead, so size it through the database specification. You can also tune the sidecar with pooler_cpu_request, pooler_cpu_limit, pooler_memory_request, and pooler_memory_limit, each a Kubernetes quantity such as 200m or 128Mi.

Extensions

PostgreSQL databases accept extensions. Read the installable names from the data source rather than guessing:

Terraform
data "appwrite_postgresql_extensions" "main" {
database_id = appwrite_postgresql_database.main.id
}
output "available_extensions" {
value = data.appwrite_postgresql_extensions.main.available
}
resource "appwrite_postgresql_extension" "postgis" {
database_id = appwrite_postgresql_database.main.id
name = "postgis"
}

The data source also returns installed and a metadata list describing each available extension.

Branches

A branch is a copy of a database. It shares the parent's credentials but has its own host and database name, which gives a preview environment or a CI job production-like data.

Terraform
resource "appwrite_postgresql_branch" "preview" {
database_id = appwrite_postgresql_database.main.id
branch_id = "preview"
}
# A branch with a TTL is reclaimed by the server when it expires.
resource "appwrite_postgresql_branch" "ephemeral" {
database_id = appwrite_postgresql_database.main.id
branch_id = "ci-run"
ttl = 3600
}
output "preview_connection_string" {
value = appwrite_postgresql_branch.preview.connection_string
sensitive = true
}

Branches have no update route, so changing any argument replaces the branch and discards its data. When a ttl expires, the server deletes the branch. The next refresh drops it from state and the following plan recreates it, so a short-lived branch comes back instead of staying deleted.

Backups

Dedicated databases use their own engine-specific backup policy resource. Use appwrite_backup_policy for databases on Appwrite's shared infrastructure instead.

Terraform
resource "appwrite_postgresql_backup_policy" "nightly" {
database_id = appwrite_postgresql_database.main.id
name = "nightly"
schedule = "0 3 * * *"
retention = 7
type = "full"
}
resource "appwrite_postgresql_backup_policy" "incremental" {
database_id = appwrite_postgresql_database.main.id
name = "six-hourly"
schedule = "0 */6 * * *"
retention = 3
type = "incremental"
}

retention is in days and schedule is a CRON expression in UTC. Changing type replaces the policy.

To find a backup to restore from, list them:

Terraform
data "appwrite_postgresql_backups" "main" {
database_id = appwrite_postgresql_database.main.id
queries = ["equal(\"status\", \"completed\")"]
}

Each backup reports id, status, type, requested_type, trigger, size_bytes, policy_id, log_position, and its timestamps. requested_type differs from type when the server could not run the backup that was asked for and fell back to another, and fallback_reason explains why.

Backup storage

Send backups to a bucket you control so they outlive the Appwrite project and fall under your own retention rules.

Terraform
resource "appwrite_postgresql_backup_storage" "offsite" {
database_id = appwrite_postgresql_database.main.id
storage_provider = "s3"
bucket = "acme-database-backups"
region = "eu-west-1"
prefix = "postgresql/main"
access_key = var.backup_access_key
secret_key = var.backup_secret_key
}

storage_provider is s3 (Amazon S3 or S3-compatible), gcs, or azure. An S3-compatible provider that is not Amazon needs an explicit endpoint.

Reading live status

The status data source reports what the database is doing right now, which is useful for alerting outputs or for gating a dependent resource.

Terraform
data "appwrite_postgresql_database_status" "main" {
database_id = appwrite_postgresql_database.main.id
}
output "database_health" {
value = {
health = data.appwrite_postgresql_database_status.main.health
ready = data.appwrite_postgresql_database_status.main.ready
connections = data.appwrite_postgresql_database_status.main.connections_current
sync_degraded = data.appwrite_postgresql_database_status.main.sync_degraded
}
}

health is healthy, degraded, unhealthy, or unknown when nothing could be measured. The replicas list reports each member's role, healthy, replicating, and lag_seconds, and volumes reports mount paths and usage. sync_state_confirmed says whether the replication fields come from an engine reading rather than a recorded estimate. false means no reading was taken, not that replication is unhealthy.

Looking up existing databases

Terraform
data "appwrite_postgresql_database" "existing" {
id = "main"
}
data "appwrite_mysql_databases" "ready" {
queries = ["equal(\"status\", \"ready\")"]
}
output "ready_database_count" {
value = data.appwrite_mysql_databases.ready.total
}

Importing

You can import most dedicated database resources. Identifiers start with the database ID:

Bash
terraform import appwrite_postgresql_database.main <database-id>
terraform import appwrite_postgresql_pooler.main <database-id>
terraform import appwrite_postgresql_extension.postgis <database-id>/postgis
terraform import appwrite_postgresql_branch.preview <database-id>/<branch-id>
terraform import appwrite_postgresql_backup_policy.nightly <database-id>/<policy-id>

Backup storage is the exception. It has no read route, so it cannot be imported.

  • DocumentsDB: schemaless JSON collections
  • VectorsDB: embeddings searched by similarity
  • TablesDB: the relational product on shared infrastructure
  • Backups: backup policies for shared databases
  • Configuration: authentication, endpoints, and timeouts

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.