diff --git a/src/routes/blog/post/announcing-dedicated-databases/+page.markdoc b/src/routes/blog/post/announcing-dedicated-databases/+page.markdoc new file mode 100644 index 0000000000..425d11140a --- /dev/null +++ b/src/routes/blog/post/announcing-dedicated-databases/+page.markdoc @@ -0,0 +1,127 @@ +--- +layout: post +title: "Announcing Dedicated databases: managed PostgreSQL, MySQL, MariaDB, and MongoDB on Appwrite" +description: Run production database engines on Appwrite Cloud. PostgreSQL, MySQL, MariaDB, and MongoDB with high availability, point-in-time recovery, branching, extensions, and a connection pooler, billed by the hour from $13/mo. +date: 2026-05-21 +cover: /images/blog/announcing-dedicated-databases/cover.avif +timeToRead: 8 +author: jake-barnby +category: announcement +featured: true +callToAction: true +faqs: + - question: "How is this different from Appwrite Databases?" + answer: "Appwrite Databases is the serverless TablesDB API with rows, columns, queries, and permissions, accessible through the Appwrite SDKs. Dedicated databases are raw PostgreSQL, MySQL, MariaDB, or MongoDB engines that you connect to with their native drivers. The products are independent, many teams will use both. Use TablesDB for app data that benefits from Appwrite's permission model; use a dedicated database when you need raw SQL, extensions like PostGIS or pgvector, or compatibility with an existing tool." + - question: "Which plans include dedicated databases?" + answer: "The Pro plan and higher. A payment method must be attached to the team. The Free specification (shared, scale-to-zero) is included with Pro at no extra cost; paid specifications are billed by the hour against the per-specification monthly price." + - question: "What engines and versions are supported?" + answer: "PostgreSQL 17 and 18 (default 18), MySQL 8.0 and 8.4 (default 8.4), MariaDB 10.11 and 11.4 (default 11.4), and MongoDB 7.0 and 8.0 (default 8.0). Version upgrades and resize operations are online, data streams to a new instance via engine-native logical replication and traffic cuts over once replication is caught up, with no read or write outages." + - question: "Can I bring my own backup storage bucket?" + answer: "Yes. The backup storage endpoint accepts S3-compatible credentials, AWS S3, Backblaze, DigitalOcean Spaces, Linode, Wasabi, GCS via S3 interop, Azure Blob via S3 interop. Backups go directly to your bucket and never enter Appwrite-managed storage. Credentials are encrypted at rest." + - question: "How does failover work?" + answer: "Appwrite continuously watches the primary. After two consecutive failed health checks (3-second timeout each), the platform promotes the replica with the lowest replication lag and emits a `failover.completed` event. A 60-second cooldown prevents flapping. You can also trigger a manual failover through the API, optionally targeting a specific replica." + - question: "Is read/write splitting automatic?" + answer: "When HA is enabled and the connection pooler is on, read/write splitting is on by default. The pooler inspects each query and routes writes plus transactions to the primary, ambient SELECTs to replicas. Turn it off if your workload requires every read to see the just-committed write." + - question: "Which region does my dedicated database live in?" + answer: "A dedicated database lives in the same region as the project that owns it, there's no per-database region selector. That means it sits next to your Functions, Sites, and Storage on the regional private network with no extra configuration." + - question: "How is billing pro-rated?" + answer: "Specification cost is pro-rated by the number of active days in the billing period, a database created mid-month is billed only from its creation timestamp to the end of the period, and a deleted database is billed only up to the deletion timestamp. Add-ons (HA, PITR, cross-region) follow the same pro-ration. Storage and bandwidth overage are computed against the included allowance for the specification." +--- + +Today we're announcing **Dedicated databases** on Appwrite Cloud. You can now run managed PostgreSQL, MySQL, MariaDB, and MongoDB engines next to your existing Appwrite Functions, Sites, and Storage, with the same per-region deployment, the same single payment relationship, and the same Console. + +Dedicated databases are a separate product from Appwrite's serverless data APIs, including [TablesDB](/docs/products/databases), DocumentsDB, and VectorsDB. Those products give you SDK-first APIs, platform permissions, and shared/serverless scaling. Dedicated databases give you raw engines for SQL, the PostgreSQL extension ecosystem, your existing ORM, or wire-compatibility with another tool. + +# What's in the box + +Each paid dedicated database is a real database engine running on its own dedicated compute, with its own storage, its own credentials, and its own public hostname. The free specification uses shared compute that scales to zero. In both cases, Appwrite manages the surrounding platform. + +**Four engines:** + +- **PostgreSQL 17 / 18**, with the standard contrib extensions plus a curated catalog of community extensions, PostGIS, pgvector, TimescaleDB, pg_stat_statements, pg_trgm, and more. +- **MySQL 8.0 / 8.4**, with semi-synchronous replication and connection pooling. +- **MariaDB 10.11 / 11.4**, with the same replication and pooling story as MySQL. +- **MongoDB 7.0 / 8.0**, with replica-set high availability via Raft. + +**Eight paid specifications** from `s-1vcpu-1gb` at $13/month to `s-8vcpu-64gb` at $699/month, plus a free shared, scale-to-zero specification included with the Pro plan. Every paid specification gets dedicated CPU that never spins down, with included storage (10 GB to 3 TB) and included bandwidth (50 GB to 10 TB) baked into the monthly price. + +**Six regions** available today, Frankfurt, New York, San Francisco, Singapore, Sydney, Toronto, with Amsterdam, London, and Bangalore in the rollout queue. A dedicated database lives in the same region as the project that owns it, so it sits next to your Functions, Sites, and Storage on the regional private network with no extra configuration. + +# Production from day one + +The features that you'd normally bolt on after the fact, HA, PITR, branching, pooling, are all part of the product. + +## High availability + +Enable HA with one update call and the platform provisions the replicas, places them on different physical hosts from the primary, and turns on engine-native replication: WAL streaming for PostgreSQL, semi-synchronous binlog for MySQL/MariaDB, and Raft oplog tailing for MongoDB. Sync mode is `async`, `sync`, or `quorum` (PostgreSQL/MySQL) so you pick the durability/latency point that fits your workload. + +When the primary stays unhealthy across two 3-second probes, Appwrite takes a 15-second distributed lock, selects the lowest-lag replica, promotes it in place (no restart for SQL engines), runs a write-verification probe, drains connections from the old primary, and emits a `failover.completed` event to your webhooks, realtime channels, and functions. There's a 60-second cooldown so transient infrastructure events don't cause failover flapping. + +Up to five replicas per database. Regional HA replicas are billed at 50% of the underlying specification per replica. + +## Backups and point-in-time recovery + +Every database supports scheduled backups with cron syntax, retention from one to 365 days, and on-demand verification, Appwrite re-downloads the backup, decrypts it in an isolated environment, and runs a sanity check (`pg_restore --list`, `mongorestore --dryRun`, …) to confirm it would actually restore. A backup that's never been verified is a hope, not a backup. + +Backups are AES-256 encrypted before they leave the database. The encryption key is unique per database and stored in Appwrite's encrypted secret store scoped to the database, so the cloud storage provider only ever sees ciphertext. You can use Appwrite-managed storage or bring your own S3-compatible bucket, AWS, Backblaze, DigitalOcean Spaces, Linode, Wasabi, GCS via S3 interop, Azure Blob via S3 interop. + +For workloads that can't afford to lose the last hour of writes between scheduled backups, **point-in-time recovery** layers continuous WAL/binlog/oplog archiving on top. Enable PITR (20% of the specification price) and you can restore to any second within the PITR window (up to 35 days). The restore produces a new database; the source is untouched, so you can validate the recovery before swapping traffic to it. + +## Branching + +A **branch** is a near-instant ephemeral copy of a dedicated database, created from a storage snapshot of the parent. The parent is briefly quiesced (`CHECKPOINT` on PostgreSQL, `FLUSH TABLES WITH READ LOCK` on MySQL, `db.fsyncLock()` on MongoDB, under a second in practice), the snapshot is taken, the parent resumes, and the branch comes up with its own credentials and its own hostname. + +Branches are the right answer to "I want to test this migration without a maintenance window" or "I want a preview database per pull request" or "I want to throw a heavy `EXPLAIN ANALYZE` at production data without touching production". The parent and the branch share storage cost-free at the moment of branching (copy-on-write); cost accumulates only as the two databases diverge. + +Branches are billed like a standalone dedicated database against their own specification, so a small branch sitting next to a large parent is cheap. + +## Connection pooler + +Each dedicated database can run its own pooler, PostgreSQL on port 6432, MySQL/MariaDB on port 6033. The pooler is enabled by default on provision and is configurable per database, pool mode (`transaction` / `session`), max client connections, default backend pool size. + +When HA is enabled, the pooler also handles **read/write splitting**: a query parser inspects each statement, pins transactions and writes to the primary, and routes ambient `SELECT`s to replicas. The application driver sees a single hostname and port; everything else is transparent. No SDK changes. + +MongoDB doesn't have a pooler equivalent in this catalog, its driver does connection management client-side, and `readPreference: 'secondary'` does the read routing. + +## SQL API + +For environments where opening a TCP connection isn't practical, Cloudflare Workers, Vercel Edge Functions, Deno Deploy, ad-hoc admin scripts, every dedicated database can also expose a **managed SQL API** over HTTPS. POST a parameterised statement with bindings, get back a JSON result. Schema-altering statements (`DROP`, `ALTER`, `GRANT`, …) are rejected unconditionally; DML statement types are gated by a per-database whitelist. Hard caps on rows, bytes, and execution time prevent runaway queries from taking down the engine. + +The SQL API is opt-in per database and disabled by default. + +# How connection works + +Each database gets a hostname of the form `db--..appwrite.network`. Resolve it with any DNS resolver, connect with `psql`, `mysql`, `mongosh`, or any driver in any language. TLS is enforced, `sslmode=require`, `useSSL=true`, `tls=true`, and the certificate is signed by a public CA, so no custom bundle is required. + +Behind the hostname sits Appwrite's edge proxy. It parses the engine's startup packet (PostgreSQL `StartupMessage`, MySQL handshake, MongoDB `OP_MSG hello`) to identify the database, applies your IP allowlist, and forwards the connection to the engine. For shared (free) databases, the proxy also detects the cold-start case: if the database has scaled to zero, the proxy triggers a start, waits for readiness, and forwards the handshake. The underlying storage is preserved across spin-downs, so no data is lost. + +# Pricing without surprises + +Three lines on the invoice cover the bulk of the cost: + +- **Specification cost**, pro-rated by active days in the billing period. A database created mid-month bills only from its creation timestamp. +- **Storage overage**, $0.125 per GB per month above the included allowance for the specification. +- **Bandwidth overage**, $0.08 per GB per month above the included allowance. + +Feature add-ons (HA replicas, cross-region replicas, cross-region standby, PITR) are billed as a percentage of the underlying specification price, pro-rated the same way. PostgreSQL extensions are free. + +# Get started + +Dedicated databases are available now on Appwrite Cloud for all Pro plan teams. The fastest path: + +1. Open your project in the Appwrite Console. +2. Open the **Databases** section in the sidebar. +3. Click **Create database** and choose the **Dedicated** type. +4. Pick an engine, version, and specification, the database is provisioned in your project's region. +5. Connect with `psql`, `mysql`, or `mongosh`. + +For the programmatic flow, the [Connect documentation](/docs/products/databases/dedicated/connect) shows credential retrieval and connection examples in Node, Python, PHP, and Go. The [Specifications page](/docs/products/databases/dedicated/specifications) has the full price list and resource limits. + +# Resources + +- [Dedicated databases documentation](/docs/products/databases/dedicated) +- [Specifications and pricing](/docs/products/databases/dedicated/specifications) +- [High availability](/docs/products/databases/dedicated/high-availability) +- [Backups and PITR](/docs/products/databases/dedicated/backups) +- [Branches](/docs/products/databases/dedicated/branches) +- [Join the Appwrite Discord community](https://appwrite.io/discord) diff --git a/src/routes/changelog/(entries)/2026-05-21.markdoc b/src/routes/changelog/(entries)/2026-05-21.markdoc new file mode 100644 index 0000000000..5edd5619b5 --- /dev/null +++ b/src/routes/changelog/(entries)/2026-05-21.markdoc @@ -0,0 +1,18 @@ +--- +layout: changelog +title: "Announcing Dedicated databases for Appwrite Cloud" +date: 2026-05-21 +cover: /images/blog/announcing-dedicated-databases/cover.avif +--- + +Appwrite Cloud now supports [**Dedicated databases**](/docs/products/databases/dedicated), managed PostgreSQL, MySQL, MariaDB, and MongoDB engines running next to your existing Functions, Sites, and Storage. Pick an engine (Postgres 17/18, MySQL 8.0/8.4, MariaDB 10.11/11.4, MongoDB 7.0/8.0), pick a region (Frankfurt, New York, San Francisco, Singapore, Sydney, Toronto), pick a specification from `s-1vcpu-1gb` at $13/mo to `s-8vcpu-64gb` at $699/mo, and connect with `psql`, `mysql`, `mongosh`, or any standard driver. + +High availability, point-in-time recovery, branching, PostgreSQL extensions, connection pooling with read/write split, and a managed SQL API over HTTPS are all available out of the box. Paid specifications are billed by the hour and pro-rated by active days; the Pro plan includes a free shared specification that scales to zero at no extra cost. + +{% arrow_link href="/blog/post/announcing-dedicated-databases" %} +Read the announcement +{% /arrow_link %} + +{% arrow_link href="/docs/products/databases/dedicated" %} +Dedicated databases in the docs +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/+layout.svelte b/src/routes/docs/products/databases/+layout.svelte index 87f1ffb5a3..ec407d28aa 100644 --- a/src/routes/docs/products/databases/+layout.svelte +++ b/src/routes/docs/products/databases/+layout.svelte @@ -127,6 +127,66 @@ } ] }, + { + label: 'Dedicated databases', + items: [ + { + label: 'Overview', + href: '/docs/products/databases/dedicated', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Specifications', + href: '/docs/products/databases/dedicated/specifications', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Connect', + href: '/docs/products/databases/dedicated/connect', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'High availability', + href: '/docs/products/databases/dedicated/high-availability', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Backups & PITR', + href: '/docs/products/databases/dedicated/backups', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Branches', + href: '/docs/products/databases/dedicated/branches', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Extensions', + href: '/docs/products/databases/dedicated/extensions', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Connection pooler', + href: '/docs/products/databases/dedicated/pooler', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'SQL API', + href: '/docs/products/databases/dedicated/sql-api', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Network', + href: '/docs/products/databases/dedicated/network', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Monitoring & insights', + href: '/docs/products/databases/dedicated/monitoring', + new: isNewUntil('31 Aug 2026') + } + ] + }, { label: 'References', items: [ @@ -150,7 +210,7 @@ .replace('tables', 'collections') ); - const hideSubtitleRoutes = ['offline', 'backups', 'csv-imports', 'csv-exports']; + const hideSubtitleRoutes = ['offline', 'backups', 'csv-imports', 'csv-exports', 'dedicated']; const shouldShowSubtitle = $derived( !hideSubtitleRoutes.some((segment) => page.route.id?.includes(segment)) && diff --git a/src/routes/docs/products/databases/+page.markdoc b/src/routes/docs/products/databases/+page.markdoc index 9217bde1e6..f4288a2329 100644 --- a/src/routes/docs/products/databases/+page.markdoc +++ b/src/routes/docs/products/databases/+page.markdoc @@ -1,19 +1,36 @@ --- layout: article title: Databases -description: Store and query structured data with Appwrite Databases. Databases provide performant and scalable storage for your application, business, and user data. +description: Choose the right Appwrite database product for structured app data, documents, vectors, or raw PostgreSQL, MySQL, MariaDB, and MongoDB engines. --- -Appwrite Databases let you store and query structured data. -Databases provide high-performance and scalable data storage for your key application, business, and user data. +Appwrite gives you several ways to store and query data. Use the serverless Appwrite data products when you want SDK-first APIs, permissions, and platform-managed scaling. Use [Dedicated databases](/docs/products/databases/dedicated) when you need a raw PostgreSQL, MySQL, MariaDB, or MongoDB engine with native drivers. {% info title="Looking for file storage?" %} Databases store data, if you need to store files like images, PDFs or videos, use [Appwrite Storage](/docs/products/storage). {% /info %} -You can organize data into databases, tables, and rows. You can also paginate, order, and query rows. -For complex business logic, Appwrite supports relationships to help you model your data. +# Choose a database product {% #choose %} + +| Product | Best for | Data model | Access pattern | Infrastructure | +|---------|----------|------------|----------------|----------------| +| **TablesDB** | Application records, relational-style app data, permissions, transactions | Databases, tables, rows, columns, relationships | Appwrite SDKs and REST APIs | Serverless shared Appwrite platform | +| **DocumentsDB** | Flexible JSON-like documents and document workflows | Documents and collections | Appwrite SDKs and REST APIs | Serverless shared Appwrite platform | +| **VectorsDB** | Embeddings, semantic search, and AI retrieval | Vectors with metadata | Appwrite SDKs and REST APIs | Serverless shared Appwrite platform | +| **Dedicated databases** | Existing database tools, raw SQL, native MongoDB, ORMs, migrations, extensions | PostgreSQL, MySQL, MariaDB, MongoDB | Native drivers like `psql`, `mysql`, `mongosh`, ORMs, and engine tools | Dedicated raw engine, with a free shared scale-to-zero specification on Pro | + +# TablesDB {% #tablesdb %} + +The main Databases documentation covers **TablesDB**, Appwrite's serverless structured data API. You can organize data into databases, tables, and rows. You can paginate, order, query rows, define relationships, and use Appwrite permissions to control access. {% arrow_link href="/docs/products/databases/quick-start" %} -Quick start -{% /arrow_link %} \ No newline at end of file +Start with TablesDB +{% /arrow_link %} + +# Raw database engines {% #dedicated %} + +Dedicated databases are separate from TablesDB, DocumentsDB, and VectorsDB. They give you managed PostgreSQL, MySQL, MariaDB, or MongoDB engines that you connect to with native tools and drivers. + +{% arrow_link href="/docs/products/databases/dedicated" %} +Explore Dedicated databases +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/dedicated/+page.markdoc b/src/routes/docs/products/databases/dedicated/+page.markdoc new file mode 100644 index 0000000000..e3a016b29b --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/+page.markdoc @@ -0,0 +1,117 @@ +--- +layout: article +title: Dedicated databases +description: Run managed PostgreSQL, MySQL, MariaDB, and MongoDB databases on Appwrite. Production-ready compute, storage, backups, high availability, and global regions, billed by the hour. +--- + +Appwrite **Dedicated databases** lets you run managed PostgreSQL, MySQL, MariaDB, and MongoDB instances on Appwrite Cloud. You pick the engine and compute size; Appwrite provisions a raw database engine in your project's region, with its own storage, networking, and credentials, and exposes it to your application over the public internet or your own private network. + +Dedicated databases are different from Appwrite's serverless/shared data products: TablesDB for tables and rows, DocumentsDB for document workflows, and VectorsDB for embeddings and semantic search. Those products are accessed through Appwrite SDKs and platform APIs. Dedicated databases give you the raw engine; you connect with `psql`, `mysql`, `mongosh`, or any driver of your choice and use the feature set of the underlying database. + +{% info title="Looking for serverless Appwrite data APIs?" %} +If you want Appwrite SDKs, platform permissions, and serverless/shared scaling for app data, start with [Appwrite Databases](/docs/products/databases). Dedicated databases are for teams who want a managed PostgreSQL, MySQL, MariaDB, or MongoDB engine they can talk to directly with their own ORM, migrations, and drivers. +{% /info %} + +# Supported engines {% #engines %} + +Appwrite manages the database container, storage, backups, and networking. You bring the application. + +| Engine | Default version | Other supported versions | Default port | +|------------|-----------------|--------------------------|--------------| +| PostgreSQL | 18 | 17 | 5432 | +| MySQL | 8.4 | 8.0 | 3306 | +| MariaDB | 11.4 | 10.11 | 3306 | +| MongoDB | 8.0 | 7.0 | 27017 | + +Version is selected on create and can be changed later via the upgrade endpoint. Upgrades run online, a second instance is provisioned on the new version, data is streamed over with engine-native logical replication, and traffic cuts over once replication is caught up. There are no read or write outages during the upgrade, and no manual dump-and-restore. + +# Database types {% #types %} + +A database is either **shared** or **dedicated**: + +| Type | Lifecycle | Resources | Use case | +|---------------|------------------------------------------|---------------------------------|---------------------------| +| **Shared** | Scales to zero after 15 minutes idle | 0.125 CPU, 128 MB RAM, 1 GB disk | Development, previews, low traffic | +| **Dedicated** | Always-on, with optional HA replicas | Any paid specification | Production workloads | + +Shared databases use the **Free** specification included with the Pro plan. They are not always-on dedicated compute: the engine runs on shared capacity, scales to zero after an idle period, and cold-starts on the next connection. The underlying storage is preserved between spin-downs, so no data is lost. + +Paid dedicated databases are billed per hour against one of eight paid specifications and never spin down. They also unlock features that the shared specification does not have access to: high availability, point-in-time recovery, configurable backup schedules, storage autoscaling, IP allowlists, and connection pooling. + +# Regions {% #regions %} + +A dedicated database lives in the same region as the project that owns it, there's no per-database region selector. Dedicated databases are available in every Appwrite region: + +| Region | Code | Endpoint | +|---------------|------|----------------------------------------------| +| Frankfurt | FRA | `db--.fra.appwrite.network` | +| New York | NYC | `db--.nyc.appwrite.network` | +| San Francisco | SFO | `db--.sfo.appwrite.network` | +| Singapore | SGP | `db--.sgp.appwrite.network` | +| Sydney | SYD | `db--.syd.appwrite.network` | +| Toronto | TOR | `db--.tor.appwrite.network` | + +Each region is independent, data does not leave the region unless you explicitly opt into the cross-region failover feature. + +# Feature overview {% #features %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/specifications" title="Specifications" %} +Eight paid specifications from $13 to $699 per month, plus a free shared specification on Pro. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Connect with `psql`, `mysql`, `mongosh`, or any driver. Credentials are rotatable from the API. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/high-availability" title="High availability" %} +Up to five synchronous, semi-synchronous, or asynchronous replicas with automatic failover. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/backups" title="Backups & PITR" %} +Scheduled encrypted backups with up to 365 days of retention and 35 days of point-in-time recovery. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Spin up an ephemeral copy of your database in seconds from a storage snapshot. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/extensions" title="Extensions" %} +Install PostgreSQL extensions like PostGIS, pgvector, and TimescaleDB from the API. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Per-database connection pooler with automatic read/write split when HA is enabled. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/sql-api" title="SQL API" %} +Run parameterised SQL over HTTP without opening a TCP connection. Useful from edge runtimes. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS by default, optional IP allowlists, and a dedicated hostname per database. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/monitoring" title="Monitoring & insights" %} +CPU, memory, IOPS, slow queries, and index recommendations, pushed to the Console live. +{% /cards_item %} +{% /cards %} + +# Limits {% #limits %} + +The following per-database limits are enforced by the API: + +| Limit | Value | +|--------------------------------|-----------------------------------------------------------| +| Databases per project | 10 | +| CPU range | 0.125 – 16 vCPU | +| Memory range | 128 MB – 64 GB | +| Storage range | 1 GB – 16 TB | +| High availability replicas | 0 – 5 | +| IP allowlist entries | 100 | +| Backup retention | 1 – 365 days | +| Point-in-time recovery window | 1 – 35 days | +| Database extensions installed | 50 (PostgreSQL only) | +| Max simultaneous connections | 10,000 platform cap, lower on smaller specifications | +| Idle timeout (shared only) | 5 – 60 minutes | + +# Billing & plan requirements {% #plans %} + +Dedicated databases are available on the **Pro** plan and higher. A payment method must be attached to the team. Shared (free specification) databases are included with the Pro plan at no extra cost. + +Each dedicated database is billed by the hour against its specification price, with separate line items for storage and bandwidth overage and percentage-based add-ons for HA, cross-region replication, and point-in-time recovery. See the [specifications page](/docs/products/databases/dedicated/specifications) for the full price list. + +{% arrow_link href="/docs/products/databases/dedicated/specifications" %} +View specifications and pricing +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/dedicated/backups/+page.markdoc b/src/routes/docs/products/databases/dedicated/backups/+page.markdoc new file mode 100644 index 0000000000..7daeaa2ba2 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/backups/+page.markdoc @@ -0,0 +1,1619 @@ +--- +layout: article +title: Backups & PITR +description: Encrypted scheduled backups with up to 365 days of retention, on-demand manual backups, and point-in-time recovery to any second within the PITR window. +--- + +Every dedicated database supports automated backups out of the box. Backups are AES-256 encrypted at rest, uploaded to an object store, and verifiable on demand. Point-in-time recovery (PITR) layers continuous WAL/binlog/oplog archiving on top, so you can restore to any second within the retention window, not just to a backup checkpoint. + +# Backup policy {% #policy %} + +Backup settings are modeled as policies. A database can have multiple policies, so you can keep short-lived hourly backups, longer-lived daily backups, and monthly checkpoints without forcing every schedule into one retention window. + +| Field | Purpose | +|-------|---------| +| `enabled` | Turns the policy schedule on or off | +| `schedule` | Cron expression for the policy | +| `retention` | Number of days backups created by the policy are retained | +| `resourceId` | Database the policy applies to | +| `resourceType` | `dedicatedDatabases` | +| `type` | Backup type, `full` or `incremental` | + +The Console can present presets such as hourly, daily, weekly, or monthly, plus a custom cron flow. Internally these policies reuse the same project-level backup policy model and scheduler as Appwrite project backups, so scheduled runs and plan limits stay consistent. + +Create a policy through the dedicated database backup policy endpoint: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "policyId": "unique()", + "name": "Hourly backups", + "schedule": "0 * * * *", + "retention": 3, + "type": "full", + "enabled": true + }' \ + https://.cloud.appwrite.io/v1/compute/databases//backups/policies +``` + +Point-in-time recovery is database-level configuration. Use `backupPitr` and `pitrRetentionDays` on the database when PITR should be enabled alongside scheduled policies. + +# What gets backed up {% #scope %} + +A backup captures the full contents of the database: all schemas, tables/collections, indexes, extensions, and (optionally) user-defined roles. The backup is taken with the engine's native tool, `pg_dump` / `mysqldump` / `mongodump`, in a dedicated job that runs alongside (not on top of) the database, so production traffic is unaffected. + +# Schedules {% #schedule %} + +Each policy has its own cron expression and retention window. The default database cron is `0 3 * * *` (daily at 03:00 UTC), jittered to spread load across the platform. The examples below configure the database-level backup defaults used for compatibility and PITR. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + backupEnabled: true, + backupCron: '0 3 * * *', + backupRetentionDays: 14, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + backupEnabled: true, + backupCron: '0 3 * * *', + backupRetentionDays: 14, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + backupEnabled: true, + backupCron: '0 3 * * *', + backupRetentionDays: 14, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + backup_enabled=True, + backup_cron='0 3 * * *', + backup_retention_days=14, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + backup_enabled: true, + backup_cron: '0 3 * * *', + backup_retention_days: 14, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + backupEnabled: true, + backupCron: "0 3 * * *", + backupRetentionDays: 14 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + backupEnabled: true, + backupCron: '0 3 * * *', + backupRetentionDays: 14, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + backupEnabled = true, + backupCron = "0 3 * * *", + backupRetentionDays = 14, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + backupEnabled: true, + backupCron: "0 3 * * *", + backupRetentionDays: 14 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseBackupEnabled(true), + compute.WithUpdateDatabaseBackupCron("0 3 * * *"), + compute.WithUpdateDatabaseBackupRetentionDays(14), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .backup_enabled(true) + .backup_cron("0 3 * * *") + .backup_retention_days(14) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "backupEnabled": true, + "backupCron": "0 3 * * *", + "backupRetentionDays": 14 + }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +Backup retention accepts 1–365 days. Backups older than the retention window are deleted automatically once a day. + +# Manual backups {% #manual %} + +To create an ad-hoc backup, before a destructive migration, for example, call the create endpoint. The response contains the backup ID; the actual dump runs asynchronously. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const backup = await compute.createDatabaseBackup({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const backup = await compute.createDatabaseBackup({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$backup = $compute->createDatabaseBackup(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +backup = compute.create_database_backup(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +backup = compute.create_database_backup(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var backup = await compute.CreateDatabaseBackup(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final backup = await compute.createDatabaseBackup( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val backup = compute.createDatabaseBackup( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let backup = try await compute.createDatabaseBackup( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + backup, err := compute.CreateDatabaseBackup("") + if err != nil { + panic(err) + } + _ = backup +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let backup = compute.create_database_backup("").await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//backups +``` +{% /multicode %} + +Manual backups use the database's default backup retention. Scheduled backups use the retention window on the policy that created them. + +# Encryption {% #encryption %} + +Backups are encrypted before they leave the database. A unique 32-byte AES-256 key is generated for the database on first backup and stored in Appwrite's encrypted secret store, scoped to the database. The dump is piped through `openssl enc -aes-256-cbc -pbkdf2` and uploaded to the configured storage target. + +Restore reverses the pipeline: the key is fetched from the secret store, the ciphertext is decrypted, and the engine restore tool consumes the plaintext stream. The key never leaves Appwrite-controlled infrastructure. + +# Storage targets {% #storage %} + +By default, backups are stored on Appwrite-managed S3-compatible storage. You can also bring your own bucket, S3, Backblaze, DigitalOcean Spaces, Linode, Wasabi, GCS via S3 interop, or Azure Blob via S3 interop. Bring-your-own-bucket backups never enter Appwrite-controlled storage. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabaseBackupStorage({ + databaseId: '', + provider: 's3', + bucket: 'my-db-backups', + region: 'eu-central-1', + prefix: 'appwrite/', + accessKey: '', + secretKey: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabaseBackupStorage({ + databaseId: '', + provider: 's3', + bucket: 'my-db-backups', + region: 'eu-central-1', + prefix: 'appwrite/', + accessKey: '', + secretKey: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabaseBackupStorage( + databaseId: '', + provider: 's3', + bucket: 'my-db-backups', + region: 'eu-central-1', + prefix: 'appwrite/', + accessKey: '', + secretKey: '', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database_backup_storage( + database_id='', + provider='s3', + bucket='my-db-backups', + region='eu-central-1', + prefix='appwrite/', + access_key='', + secret_key='', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database_backup_storage( + database_id: '', + provider: 's3', + bucket: 'my-db-backups', + region: 'eu-central-1', + prefix: 'appwrite/', + access_key: '', + secret_key: '', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabaseBackupStorage( + databaseId: "", + provider: "s3", + bucket: "my-db-backups", + region: "eu-central-1", + prefix: "appwrite/", + accessKey: "", + secretKey: "" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabaseBackupStorage( + databaseId: '', + provider: 's3', + bucket: 'my-db-backups', + region: 'eu-central-1', + prefix: 'appwrite/', + accessKey: '', + secretKey: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabaseBackupStorage( + databaseId = "", + provider = "s3", + bucket = "my-db-backups", + region = "eu-central-1", + prefix = "appwrite/", + accessKey = "", + secretKey = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabaseBackupStorage( + databaseId: "", + provider: "s3", + bucket: "my-db-backups", + region: "eu-central-1", + prefix: "appwrite/", + accessKey: "", + secretKey: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabaseBackupStorage("", + compute.WithUpdateDatabaseBackupStorageProvider("s3"), + compute.WithUpdateDatabaseBackupStorageBucket("my-db-backups"), + compute.WithUpdateDatabaseBackupStorageRegion("eu-central-1"), + compute.WithUpdateDatabaseBackupStoragePrefix("appwrite/"), + compute.WithUpdateDatabaseBackupStorageAccessKey(""), + compute.WithUpdateDatabaseBackupStorageSecretKey(""), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database_backup_storage("") + .provider("s3") + .bucket("my-db-backups") + .region("eu-central-1") + .prefix("appwrite/") + .access_key("") + .secret_key("") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PUT \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "provider": "s3", + "bucket": "my-db-backups", + "region": "eu-central-1", + "prefix": "appwrite/", + "accessKey": "", + "secretKey": "" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//backups/storage +``` +{% /multicode %} + +Credentials are encrypted at rest. The `endpoint` field is supported for S3-compatible providers, leave empty for AWS S3. + +# Verify a backup {% #verify %} + +A backup that has never been verified is a hope, not a backup. Appwrite exposes a verification endpoint that downloads the backup, decrypts it, and runs an engine-native sanity check (`pg_restore --list`, `mysql --execute "..."`, `mongorestore --dryRun`) in an isolated environment, then tears the temporary resources down. + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//backups//verifications +``` + +The result is recorded on the backup document under `verifiedAt`. You can query the backup later to see verification history. + +# Restore from a backup {% #restore %} + +Restoring from a backup creates a new dedicated database, applies the dump, and waits for the engine to come up. The original database is untouched. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseRestoration({ + databaseId: '', + type: 'backup', + backupId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseRestoration({ + databaseId: '', + type: 'backup', + backupId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->createDatabaseRestoration( + databaseId: '', + type: 'backup', + backupId: '', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.create_database_restoration( + database_id='', + type='backup', + backup_id='', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.create_database_restoration( + database_id: '', + type: 'backup', + backup_id: '', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.CreateDatabaseRestoration( + databaseId: "", + type: "backup", + backupId: "" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.createDatabaseRestoration( + databaseId: '', + type: 'backup', + backupId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.createDatabaseRestoration( + databaseId = "", + type = "backup", + backupId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.createDatabaseRestoration( + databaseId: "", + type: "backup", + backupId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.CreateDatabaseRestoration("", + compute.WithCreateDatabaseRestorationType("backup"), + compute.WithCreateDatabaseRestorationBackupId(""), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_restoration("") + .r#type("backup") + .backup_id("") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "type": "backup", + "backupId": "" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//restorations +``` +{% /multicode %} + +The new database is provisioned with the same specification as the source unless you also pass overrides for `cpu`, `memory`, and `storage`. + +# Point-in-time recovery {% #pitr %} + +PITR layers continuous transaction-log archiving on top of scheduled backups. With PITR enabled, the database keeps shipping WAL segments (PostgreSQL), binlog files (MySQL/MariaDB), or oplog entries (MongoDB) to object storage every five minutes. To restore, the platform replays the most recent base backup plus the WAL up to the target timestamp. + +Enable PITR on create or update: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + backupPitr: true, + pitrRetentionDays: 7, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + backupPitr: true, + pitrRetentionDays: 7, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + backupPitr: true, + pitrRetentionDays: 7, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + backup_pitr=True, + pitr_retention_days=7, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + backup_pitr: true, + pitr_retention_days: 7, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + backupPitr: true, + pitrRetentionDays: 7 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + backupPitr: true, + pitrRetentionDays: 7, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + backupPitr = true, + pitrRetentionDays = 7, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + backupPitr: true, + pitrRetentionDays: 7 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseBackupPitr(true), + compute.WithUpdateDatabasePitrRetentionDays(7), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .backup_pitr(true) + .pitr_retention_days(7) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ "backupPitr": true, "pitrRetentionDays": 7 }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +`pitrRetentionDays` accepts 1–35 days. PITR is billed as a feature add-on at 20% of the underlying specification price. + +# Recovery windows {% #windows %} + +Each database with PITR enabled exposes its current recovery window. Use this to confirm the earliest and latest timestamps you can restore to before initiating a recovery: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const windows = await compute.getDatabasePITRWindows({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const windows = await compute.getDatabasePITRWindows({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$windows = $compute->getDatabasePITRWindows(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +windows = compute.get_database_pitr_windows(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +windows = compute.get_database_pitr_windows(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var windows = await compute.GetDatabasePITRWindows(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final windows = await compute.getDatabasePITRWindows( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val windows = compute.getDatabasePITRWindows( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let windows = try await compute.getDatabasePITRWindows( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + windows, err := compute.GetDatabasePITRWindows("") + if err != nil { + panic(err) + } + _ = windows +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let windows = compute.get_database_pitr_windows("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//pitr +``` +{% /multicode %} + +Response: + +```json +{ + "earliestRecoveryPoint": "2026-05-14T00:00:00Z", + "latestRecoveryPoint": "2026-05-21T13:34:12Z", + "windows": [ + { "from": "2026-05-14T00:00:00Z", "to": "2026-05-21T13:34:12Z" } + ] +} +``` + +# Restore to a point in time {% #pitr-restore %} + +A PITR restore takes a target timestamp inside the active window and produces a new database that represents the state of the source at that exact moment: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseRestoration({ + databaseId: '', + type: 'pitr', + targetTime: '2026-05-21T13:30:00Z', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseRestoration({ + databaseId: '', + type: 'pitr', + targetTime: '2026-05-21T13:30:00Z', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->createDatabaseRestoration( + databaseId: '', + type: 'pitr', + targetTime: '2026-05-21T13:30:00Z', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.create_database_restoration( + database_id='', + type='pitr', + target_time='2026-05-21T13:30:00Z', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.create_database_restoration( + database_id: '', + type: 'pitr', + target_time: '2026-05-21T13:30:00Z', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.CreateDatabaseRestoration( + databaseId: "", + type: "pitr", + targetTime: "2026-05-21T13:30:00Z" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.createDatabaseRestoration( + databaseId: '', + type: 'pitr', + targetTime: '2026-05-21T13:30:00Z', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.createDatabaseRestoration( + databaseId = "", + type = "pitr", + targetTime = "2026-05-21T13:30:00Z", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.createDatabaseRestoration( + databaseId: "", + type: "pitr", + targetTime: "2026-05-21T13:30:00Z" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.CreateDatabaseRestoration("", + compute.WithCreateDatabaseRestorationType("pitr"), + compute.WithCreateDatabaseRestorationTargetTime("2026-05-21T13:30:00Z"), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_restoration("") + .r#type("pitr") + .target_time("2026-05-21T13:30:00Z") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "type": "pitr", + "targetTime": "2026-05-21T13:30:00Z" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//restorations +``` +{% /multicode %} + +The new database is independent, recovering does not roll back the source. Once you've validated the recovery, you can swap your application traffic to it or copy specific tables/collections back to the source with `pg_dump` / `mysqldump` / `mongodump`. + +# Limits {% #limits %} + +| Limit | Value | +|----------------------------------|-------------------| +| Backup retention | 1 – 365 days | +| PITR retention | 1 – 35 days | +| Manual + scheduled backups | Unlimited (subject to storage) | +| Cron schedule resolution | 1 minute | +| WAL/binlog/oplog flush cadence | every 5 minutes | + +Backup storage usage rolls up into the dedicated database storage line on your invoice, there's no separate backup storage line item. diff --git a/src/routes/docs/products/databases/dedicated/branches/+page.markdoc b/src/routes/docs/products/databases/dedicated/branches/+page.markdoc new file mode 100644 index 0000000000..258e4b4fec --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/branches/+page.markdoc @@ -0,0 +1,711 @@ +--- +layout: article +title: Branches +description: Spin up an ephemeral, isolated copy of a dedicated database in seconds from a storage snapshot. Use branches for previews, migrations, and testing. +--- + +A **branch** is a short-lived, isolated copy of a dedicated database. It has its own credentials, its own endpoint, and starts from a point-in-time snapshot of the parent's storage volume. Branches are not replicas, once created, they diverge from the parent and never sync back. + +{% info title="Branches do not merge back" %} +There is no branch merge operation. Use a branch to validate a migration, data repair, or application change, then intentionally cut application traffic over to the validated database or copy the data you want back with engine-native tools. Appwrite does not reconcile two diverged database histories for you. +{% /info %} + +Use cases: + +- **Preview environments**, one branch per pull request, destroyed when the PR closes +- **Test migrations**, apply a destructive `ALTER` against the branch first, observe behaviour, then run it against the source +- **Reproduce a bug**, branch off the most recent PITR window, attach a debugger, throw it away when done +- **Try an analytical query**, heavy `EXPLAIN ANALYZE` work against a branch can't slow down the primary + +# How it works {% #how %} + +Creating a branch: + +1. **Briefly pause writes on the parent.** PostgreSQL takes a `CHECKPOINT`. MySQL/MariaDB issue `FLUSH TABLES WITH READ LOCK`. MongoDB runs `db.fsyncLock()`. +2. **Snapshot the parent's storage volume.** This is a near-instant copy-on-write operation. +3. **Resume writes on the parent.** The parent is paused for under a second. +4. **Provision the branch** from the snapshot, its own isolated storage volume, its own database instance. +5. **Start the branch** on the same engine and version. Resources can be smaller than the parent. +6. **Generate branch credentials** and return the branch endpoint. + +The parent and the branch share storage cost-free at the moment of branching (copy-on-write). Storage cost grows as the two databases diverge. + +PostgreSQL supports crash-consistent snapshots, so the pause is a fast checkpoint and PostgreSQL branches recover via WAL replay on first start. MySQL/MariaDB and MongoDB require an explicit write freeze before snapshot to produce a clean image. + +# Create a branch {% #create %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseBranch({ + databaseId: '', + branchId: 'pr-1234', + name: 'PR 1234 preview', + cpu: 1000, + memory: 2048, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseBranch({ + databaseId: '', + branchId: 'pr-1234', + name: 'PR 1234 preview', + cpu: 1000, + memory: 2048, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->createDatabaseBranch( + databaseId: '', + branchId: 'pr-1234', + name: 'PR 1234 preview', + cpu: 1000, + memory: 2048, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.create_database_branch( + database_id='', + branch_id='pr-1234', + name='PR 1234 preview', + cpu=1000, + memory=2048, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.create_database_branch( + database_id: '', + branch_id: 'pr-1234', + name: 'PR 1234 preview', + cpu: 1000, + memory: 2048, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.CreateDatabaseBranch( + databaseId: "", + branchId: "pr-1234", + name: "PR 1234 preview", + cpu: 1000, + memory: 2048 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.createDatabaseBranch( + databaseId: '', + branchId: 'pr-1234', + name: 'PR 1234 preview', + cpu: 1000, + memory: 2048, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.createDatabaseBranch( + databaseId = "", + branchId = "pr-1234", + name = "PR 1234 preview", + cpu = 1000, + memory = 2048, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.createDatabaseBranch( + databaseId: "", + branchId: "pr-1234", + name: "PR 1234 preview", + cpu: 1000, + memory: 2048 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.CreateDatabaseBranch("", + compute.WithCreateDatabaseBranchBranchId("pr-1234"), + compute.WithCreateDatabaseBranchName("PR 1234 preview"), + compute.WithCreateDatabaseBranchCpu(1000), + compute.WithCreateDatabaseBranchMemory(2048), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_branch("") + .branch_id("pr-1234") + .name("PR 1234 preview") + .cpu(1000) + .memory(2048) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "branchId": "pr-1234", + "name": "PR 1234 preview", + "cpu": 1000, + "memory": 2048 + }' \ + https://.cloud.appwrite.io/v1/compute/databases//branches +``` +{% /multicode %} + +Optional fields: + +| Field | Default | Purpose | +|------------|-------------------------|---------------------------------------------------------| +| `cpu` | parent's CPU | Branch can be smaller (or larger) than the parent | +| `memory` | parent's memory | Same | +| `storage` | parent's storage | Branches inherit storage from the snapshot | + +The branch endpoint is exposed as a separate hostname: + +``` +db---..appwrite.network +``` + +Connect to it with the engine's standard CLI, same as the parent. The branch endpoint and credentials are returned alongside every entry of the [list branches](#list) response, indexed by `branchId`. + +# List branches {% #list %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const branches = await compute.listDatabaseBranches({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const branches = await compute.listDatabaseBranches({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$branches = $compute->listDatabaseBranches(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +branches = compute.list_database_branches(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +branches = compute.list_database_branches(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var branches = await compute.ListDatabaseBranches(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final branches = await compute.listDatabaseBranches( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val branches = compute.listDatabaseBranches( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let branches = try await compute.listDatabaseBranches( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + branches, err := compute.ListDatabaseBranches("") + if err != nil { + panic(err) + } + _ = branches +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let branches = compute.list_database_branches("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//branches +``` +{% /multicode %} + +# Delete a branch {% #delete %} + +Deleting a branch removes the branch's database instance, its storage volume, and the underlying snapshot. There's no soft-delete, once the branch is gone, the data is gone. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.deleteDatabaseBranch({ + databaseId: '', + branchId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.deleteDatabaseBranch({ + databaseId: '', + branchId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->deleteDatabaseBranch( + databaseId: '', + branchId: '', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.delete_database_branch( + database_id='', + branch_id='', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.delete_database_branch( + database_id: '', + branch_id: '', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.DeleteDatabaseBranch( + databaseId: "", + branchId: "" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.deleteDatabaseBranch( + databaseId: '', + branchId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.deleteDatabaseBranch( + databaseId = "", + branchId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.deleteDatabaseBranch( + databaseId: "", + branchId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.DeleteDatabaseBranch("", "") + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.delete_database_branch("", "") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X DELETE \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//branches/ +``` +{% /multicode %} + +# Billing {% #billing %} + +A branch is billed like a standalone dedicated database against its own specification (CPU, memory, storage). The snapshot itself is free at the moment of branching; storage cost accumulates as the branch's data diverges from the parent. + +There is no separate "branch" line item, branches roll into your regular dedicated database storage and compute totals. + +# Use case: per-PR preview database {% #ci-example %} + +A CI pipeline that branches on every pull request and tears down on merge or close: + +```yaml +# .github/workflows/preview.yml +on: + pull_request: + types: [opened, reopened, synchronize, closed] + +jobs: + preview: + runs-on: ubuntu-latest + steps: + - name: Create or update preview branch + if: github.event.action != 'closed' + run: | + curl -X POST \ + -H "X-Appwrite-Project: $PROJECT_ID" \ + -H "X-Appwrite-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "branchId": "pr-${{ github.event.number }}", "name": "PR #${{ github.event.number }}" }' \ + https://fra.cloud.appwrite.io/v1/compute/databases/$DB_ID/branches || true + + - name: Tear down preview branch on close + if: github.event.action == 'closed' + run: | + curl -X DELETE \ + -H "X-Appwrite-Project: $PROJECT_ID" \ + -H "X-Appwrite-Key: $API_KEY" \ + https://fra.cloud.appwrite.io/v1/compute/databases/$DB_ID/branches/pr-${{ github.event.number }} +``` + +The `|| true` on `POST` makes the workflow idempotent, if the branch already exists, the call is a no-op. diff --git a/src/routes/docs/products/databases/dedicated/connect/+page.markdoc b/src/routes/docs/products/databases/dedicated/connect/+page.markdoc new file mode 100644 index 0000000000..c4f44511ec --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/connect/+page.markdoc @@ -0,0 +1,537 @@ +--- +layout: article +title: Connect +description: Connect to a dedicated database with psql, mysql, mongosh, or any standard driver. Retrieve credentials from the API, manage additional users, and rotate the primary password. +--- + +A dedicated database exposes a real engine endpoint over TLS. You connect to it the same way you would connect to any PostgreSQL, MySQL, MariaDB, or MongoDB server: with the engine's standard CLI or any ORM/driver in any language. + +# Get connection details in Console {% #console %} + +The fastest way to connect is through the Appwrite Console: + +1. Open your project in the Appwrite Console. +2. Open **Databases** and select your dedicated database. +3. Open the **Connect** or **Credentials** view. +4. Copy the connection string for your engine. +5. Paste it into `psql`, `mysql`, `mongosh`, your ORM, or your database client. + +Use the API or SDK flow below when you need to fetch credentials from automation, rotate credentials from a script, or inject the connection string into your deployment pipeline. + +# Get credentials with the API {% #credentials %} + +The connection details for a dedicated database are exposed through the [credentials endpoint](/docs/references/cloud/server-rest/compute#getDatabaseCredentials). Call it from any server SDK with an API key that has the `databases.read` scope. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const credentials = await compute.getDatabaseCredentials({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const credentials = await compute.getDatabaseCredentials({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$credentials = $compute->getDatabaseCredentials(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +credentials = compute.get_database_credentials(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +credentials = compute.get_database_credentials(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var credentials = await compute.GetDatabaseCredentials(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final credentials = await compute.getDatabaseCredentials( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val credentials = compute.getDatabaseCredentials( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let credentials = try await compute.getDatabaseCredentials( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + credentials, err := compute.GetDatabaseCredentials("") + if err != nil { + panic(err) + } + _ = credentials +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let credentials = compute.get_database_credentials("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//credentials +``` +{% /multicode %} + +Response: + +```json +{ + "host": "db--..appwrite.network", + "port": 5432, + "username": "appwrite", + "password": "<32-char-hex>", + "database": "appwrite", + "engine": "postgres", + "ssl": true, + "connectionString": "postgres://appwrite:@db--..appwrite.network:5432/appwrite?sslmode=require" +} +``` + +The default database name and username on every engine are both `appwrite`. The primary password is generated on provision and stored encrypted in the platform database; you can rotate it from the API at any time without provisioning a new database. + +# Connect from a shell {% #shells %} + +PostgreSQL: + +```bash +psql "$(curl -s ... credentials | jq -r .connectionString)" +# or directly: +psql -h db--..appwrite.network -p 5432 -U appwrite -d appwrite +``` + +MySQL / MariaDB: + +```bash +mysql -h db--..appwrite.network -P 3306 -u appwrite -p appwrite +``` + +MongoDB: + +```bash +mongosh "mongodb://appwrite:@db--..appwrite.network:27017/appwrite?tls=true" +``` + +# Rotate the primary password {% #rotate %} + +The credentials endpoint also accepts `PATCH` to issue a new password for the primary user. The change is applied atomically in the underlying engine and the stored credential is updated in the same call. Existing sessions remain alive until the engine's idle timeout, then have to authenticate with the new password. The API key needs the `databases.write` scope. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const credentials = await compute.updateDatabaseCredentials({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const credentials = await compute.updateDatabaseCredentials({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$credentials = $compute->updateDatabaseCredentials(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +credentials = compute.update_database_credentials(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +credentials = compute.update_database_credentials(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var credentials = await compute.UpdateDatabaseCredentials(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final credentials = await compute.updateDatabaseCredentials( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val credentials = compute.updateDatabaseCredentials( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let credentials = try await compute.updateDatabaseCredentials( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + credentials, err := compute.UpdateDatabaseCredentials("") + if err != nil { + panic(err) + } + _ = credentials +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let credentials = compute.update_database_credentials("").await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//credentials +``` +{% /multicode %} + +# Additional connection users {% #connections %} + +The primary `appwrite` user has full ownership of the default database. For applications that need narrower roles, a read-only reporting user, a write-only ingestion user, a per-tenant user, you can create additional connections from the API: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "analytics_ro", + "database": "appwrite", + "role": "readonly" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//connections +``` + +The response contains the generated password for the new user. Appwrite stores it encrypted; if you lose it, delete the connection and create a new one. + +Available roles: + +| Role | PostgreSQL / MySQL / MariaDB | MongoDB | +|--------------|-------------------------------------------------------|-------------------------------| +| `readonly` | `SELECT` on all tables in the schema | `read` on the database | +| `readwrite` | `SELECT`, `INSERT`, `UPDATE`, `DELETE` (no schema DDL)| `readWrite` on the database | + +`readwrite` is the default. List, get, and delete connections through the same endpoints. The primary `appwrite` user cannot be deleted. + +# TLS {% #tls %} + +All dedicated database endpoints terminate TLS at the edge proxy and forward to the engine over the Appwrite network. Connection strings always include `sslmode=require` (PostgreSQL), `useSSL=true` (MySQL/MariaDB), or `tls=true` (MongoDB) by default, so your driver should not need any extra configuration. + +To require mTLS, see the [network page](/docs/products/databases/dedicated/network). + +# Cold-start (shared databases) {% #cold-start %} + +If your database is on the **Free** specification, it spins down to zero compute after 15 minutes of inactivity. The next connection is intercepted by the edge proxy, which sends a start command to the platform and waits for the database to become ready before forwarding the handshake. + +The cold-start adds a few seconds to the first query after an idle period. Subsequent queries see no extra latency. The underlying storage is preserved across spin-downs, so no data is lost. + +# Connecting from an application {% #drivers %} + +There's nothing Appwrite-specific about the driver setup. A few example snippets: + +{% multicode %} +```server-nodejs +import { Client } from 'pg'; + +const client = new Client({ + host: 'db--..appwrite.network', + port: 5432, + user: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: { rejectUnauthorized: true }, +}); + +await client.connect(); +const { rows } = await client.query('SELECT now()'); +console.log(rows); +``` +```server-python +import psycopg +import os + +with psycopg.connect( + host='db--..appwrite.network', + port=5432, + user='appwrite', + password=os.environ['DB_PASSWORD'], + dbname='appwrite', + sslmode='require', +) as conn: + with conn.cursor() as cur: + cur.execute('SELECT now()') + print(cur.fetchone()) +``` +```server-php +-..appwrite.network;port=5432;dbname=appwrite;sslmode=require', + 'appwrite', + getenv('DB_PASSWORD'), +); + +$rows = $pdo->query('SELECT now()')->fetchAll(); +print_r($rows); +``` +```server-go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + conn, _ := pgx.Connect(context.Background(), + fmt.Sprintf("postgres://appwrite:%s@db--..appwrite.network:5432/appwrite?sslmode=require", + os.Getenv("DB_PASSWORD"))) + defer conn.Close(context.Background()) + + var now string + conn.QueryRow(context.Background(), "SELECT now()").Scan(&now) + fmt.Println(now) +} +``` +{% /multicode %} + +Once you can run a query, you can use any tool that talks the engine's wire protocol: pgAdmin, DataGrip, MySQL Workbench, MongoDB Compass, your ORM of choice, your migration tool of choice, your read-replica failover library of choice. Appwrite gets out of the way. diff --git a/src/routes/docs/products/databases/dedicated/extensions/+page.markdoc b/src/routes/docs/products/databases/dedicated/extensions/+page.markdoc new file mode 100644 index 0000000000..05c0642df5 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/extensions/+page.markdoc @@ -0,0 +1,660 @@ +--- +layout: article +title: Extensions +description: Install and manage PostgreSQL extensions like PostGIS, pgvector, and TimescaleDB on a dedicated database. Extensions are managed through the API and are free of charge. +--- + +PostgreSQL exposes a rich extension ecosystem, PostGIS for geospatial data, pgvector for embeddings, TimescaleDB for time-series, pg_stat_statements for query analytics, and many more. Dedicated PostgreSQL databases on Appwrite support managing extensions through the API; there is no separate cost. + +MySQL, MariaDB, and MongoDB do not have an equivalent extension system. The extensions endpoints are available on those engines but always return an empty list. + +# Install an extension {% #install %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseExtension({ + databaseId: '', + name: 'pgvector', + version: '0.7.4', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseExtension({ + databaseId: '', + name: 'pgvector', + version: '0.7.4', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->createDatabaseExtension( + databaseId: '', + name: 'pgvector', + version: '0.7.4', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.create_database_extension( + database_id='', + name='pgvector', + version='0.7.4', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.create_database_extension( + database_id: '', + name: 'pgvector', + version: '0.7.4', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.CreateDatabaseExtension( + databaseId: "", + name: "pgvector", + version: "0.7.4" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.createDatabaseExtension( + databaseId: '', + name: 'pgvector', + version: '0.7.4', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.createDatabaseExtension( + databaseId = "", + name = "pgvector", + version = "0.7.4", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.createDatabaseExtension( + databaseId: "", + name: "pgvector", + version: "0.7.4" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.CreateDatabaseExtension("", + compute.WithCreateDatabaseExtensionName("pgvector"), + compute.WithCreateDatabaseExtensionVersion("0.7.4"), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_extension("") + .name("pgvector") + .version("0.7.4") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "name": "pgvector", + "version": "0.7.4" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//extensions +``` +{% /multicode %} + +The request enqueues a worker that runs `CREATE EXTENSION` inside the database. The response returns immediately with status `creating`; the extension transitions to `ready` once the install completes (typically within a few seconds for small extensions). + +If the extension requires a non-default schema, pass the optional `schema` field. If it's a contrib extension shipped with PostgreSQL it's available out of the box; if it's a community extension, the platform installs the system package first. + +# List extensions {% #list %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const extensions = await compute.listDatabaseExtensions({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const extensions = await compute.listDatabaseExtensions({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$extensions = $compute->listDatabaseExtensions(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +extensions = compute.list_database_extensions(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +extensions = compute.list_database_extensions(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var extensions = await compute.ListDatabaseExtensions(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final extensions = await compute.listDatabaseExtensions( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val extensions = compute.listDatabaseExtensions( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let extensions = try await compute.listDatabaseExtensions( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + extensions, err := compute.ListDatabaseExtensions("") + if err != nil { + panic(err) + } + _ = extensions +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let extensions = compute.list_database_extensions("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//extensions +``` +{% /multicode %} + +The response separates installed extensions from available extensions: + +```json +{ + "installed": [ + { "name": "pg_stat_statements", "version": "1.10", "schema": "public", "status": "ready" }, + { "name": "pgvector", "version": "0.7.4", "schema": "public", "status": "ready" } + ], + "available": [ + { "name": "postgis", "defaultVersion": "3.4", "category": "geospatial" }, + { "name": "timescaledb", "defaultVersion": "2.15", "category": "timeseries" }, + { "name": "pg_trgm", "defaultVersion": "1.6", "category": "text-search" } + ] +} +``` + +# Remove an extension {% #remove %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.deleteDatabaseExtension({ + databaseId: '', + extensionName: 'pgvector', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.deleteDatabaseExtension({ + databaseId: '', + extensionName: 'pgvector', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->deleteDatabaseExtension( + databaseId: '', + extensionName: 'pgvector', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.delete_database_extension( + database_id='', + extension_name='pgvector', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.delete_database_extension( + database_id: '', + extension_name: 'pgvector', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.DeleteDatabaseExtension( + databaseId: "", + extensionName: "pgvector" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.deleteDatabaseExtension( + databaseId: '', + extensionName: 'pgvector', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.deleteDatabaseExtension( + databaseId = "", + extensionName = "pgvector", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.deleteDatabaseExtension( + databaseId: "", + extensionName: "pgvector" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.DeleteDatabaseExtension("", "pgvector") + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.delete_database_extension("", "pgvector") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X DELETE \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//extensions/pgvector +``` +{% /multicode %} + +`DROP EXTENSION` is run with `RESTRICT` by default, if there are dependent objects (tables, indexes, columns), the call fails. Drop the dependents first, or call `DROP EXTENSION ... CASCADE` directly through SQL to acknowledge that you understand what gets removed. + +# Available extensions {% #available %} + +A non-exhaustive list of common extensions that are pre-packaged and installable through the API: + +| Category | Extensions | +|-------------|-------------------------------------------------------------------------------------------------------------| +| Geospatial | `postgis`, `postgis_raster`, `postgis_topology`, `pgrouting` | +| Vectors | `pgvector`, `pg_embedding` | +| Time-series | `timescaledb` | +| Analytics | `pg_stat_statements`, `auto_explain`, `pg_buffercache` | +| Text search | `pg_trgm`, `unaccent`, `fuzzystrmatch` | +| Crypto | `pgcrypto`, `uuid-ossp` | +| FDWs | `postgres_fdw`, `mysql_fdw`, `oracle_fdw` | +| HTTP | `pg_net`, `http` | +| Misc | `hstore`, `citext`, `tablefunc`, `intarray`, `ltree`, `cube`, `earthdistance` | + +If you need an extension that isn't on this list, [open a feature request](https://github.com/appwrite/appwrite/issues), the catalog grows on demand. Custom or proprietary extensions require Enterprise support. + +# Version selection {% #versions %} + +Each extension has a list of versions tied to the PostgreSQL major version of the database. Newer PostgreSQL versions support newer extension versions; downgrading the engine version can require downgrading extensions first. Use the `version` field on the install request to pin to a specific version, or omit it to install the default version for the engine. + +To upgrade an extension version, drop it and reinstall it with the new version. The drop is rejected if there are dependent objects, which is the safe default for extensions that change schema (PostGIS, TimescaleDB). + +# Limits {% #limits %} + +| Limit | Value | +|---------------------------------------------|---------------| +| Maximum extensions per database | 50 | +| Extension install/uninstall billing | Free | +| Supported engines | PostgreSQL | + +The 50-extension cap is generous, most production deployments install fewer than ten. Extensions that pull in a lot of shared library code (PostGIS, TimescaleDB) count the same as small ones. diff --git a/src/routes/docs/products/databases/dedicated/high-availability/+page.markdoc b/src/routes/docs/products/databases/dedicated/high-availability/+page.markdoc new file mode 100644 index 0000000000..1d057d4fde --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/high-availability/+page.markdoc @@ -0,0 +1,693 @@ +--- +layout: article +title: High availability +description: Run up to five replicas of a dedicated database with synchronous, semi-synchronous, or asynchronous replication and automatic failover on primary failure. +--- + +High availability adds replicas to a dedicated database and enables automatic failover when the primary becomes unhealthy. With HA on, a primary outage is recovered in seconds by promoting the most up-to-date replica, without manual intervention. + +HA is opt-in per database and requires a paid specification, the free shared specification does not support replication. + +# How it works {% #how %} + +When you enable HA, Appwrite scales the underlying database to the configured replica count and turns on engine-native replication: + +| Engine | Replication mechanism | +|------------|---------------------------------------------------------------------------------------| +| PostgreSQL | Streaming replication over WAL | +| MySQL | Semi-synchronous binlog replication | +| MariaDB | Semi-synchronous binlog replication | +| MongoDB | Replica-set oplog tailing with Raft-based primary election | + +Replicas are placed on different physical hosts from the primary, so a single host failure does not take down both the primary and a replica. + +# Sync modes {% #sync-modes %} + +PostgreSQL and MySQL/MariaDB expose three durability modes. MongoDB uses its own write-concern model and is not configurable through this setting. + +| Mode | Commit acknowledged when… | Durability | Write latency | +|--------------|------------------------------------------------------|----------------|---------------------| +| `async` | The primary persists the write to local WAL/binlog | Low | Lowest (default) | +| `sync` | At least one replica acknowledges the write | High | +network round-trip | +| `quorum` | A majority of replicas acknowledge the write | Highest | +network round-trip | + +A safe default for production is `sync` with two replicas, a single replica failure does not block writes, and you still cannot lose a committed write to a primary-only crash. + +# Enable HA {% #enable %} + +Set `highAvailability`, `highAvailabilityReplicaCount`, and `highAvailabilitySyncMode` on the database. You can do this on create, or by updating the database later: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: 'sync', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: 'sync', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: 'sync', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + high_availability=True, + high_availability_replica_count=2, + high_availability_sync_mode='sync', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + high_availability: true, + high_availability_replica_count: 2, + high_availability_sync_mode: 'sync', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: "sync" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: 'sync', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + highAvailability = true, + highAvailabilityReplicaCount = 2, + highAvailabilitySyncMode = "sync", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + highAvailability: true, + highAvailabilityReplicaCount: 2, + highAvailabilitySyncMode: "sync" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseHighAvailability(true), + compute.WithUpdateDatabaseHighAvailabilityReplicaCount(2), + compute.WithUpdateDatabaseHighAvailabilitySyncMode("sync"), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .high_availability(true) + .high_availability_replica_count(2) + .high_availability_sync_mode("sync") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "highAvailability": true, + "highAvailabilityReplicaCount": 2, + "highAvailabilitySyncMode": "sync" + }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +`highAvailabilityReplicaCount` accepts 0–5. The primary is not counted, a value of 2 gives you one primary plus two replicas. + +Switching HA on or off is asynchronous: the platform queues a worker that provisions the new replicas, waits for them to come up, then runs an initial base sync. Watch the status with: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const status = await compute.getDatabaseHAStatus({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const status = await compute.getDatabaseHAStatus({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$status = $compute->getDatabaseHAStatus(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +status = compute.get_database_ha_status(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +status = compute.get_database_ha_status(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var status = await compute.GetDatabaseHAStatus(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final status = await compute.getDatabaseHAStatus( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val status = compute.getDatabaseHAStatus( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let status = try await compute.getDatabaseHAStatus( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + status, err := compute.GetDatabaseHAStatus("") + if err != nil { + panic(err) + } + _ = status +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let status = compute.get_database_ha_status("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//ha +``` +{% /multicode %} + +The response contains per-replica state and current replication lag in seconds. + +# Automatic failover {% #failover %} + +Appwrite continuously watches the health of the primary. When the primary stays unhealthy across two consecutive health checks, the platform promotes the replica with the lowest replication lag and shifts the connection endpoint to it. Clients reconnect transparently. + +A `failover.completed` event is emitted on the database and fans out to webhooks, realtime subscriptions, and functions. A 60-second cooldown between failovers prevents thrashing during transient infrastructure events. + +# Manual failover {% #manual-failover %} + +To trigger a failover yourself, for maintenance, testing, or a controlled swap, call the failover endpoint: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseFailover({ + databaseId: '', + targetReplicaId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.createDatabaseFailover({ + databaseId: '', + targetReplicaId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->createDatabaseFailover( + databaseId: '', + targetReplicaId: '', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.create_database_failover( + database_id='', + target_replica_id='', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.create_database_failover( + database_id: '', + target_replica_id: '', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.CreateDatabaseFailover( + databaseId: "", + targetReplicaId: "" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.createDatabaseFailover( + databaseId: '', + targetReplicaId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.createDatabaseFailover( + databaseId = "", + targetReplicaId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.createDatabaseFailover( + databaseId: "", + targetReplicaId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.CreateDatabaseFailover("", + compute.WithCreateDatabaseFailoverTargetReplicaId(""), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_failover("") + .target_replica_id("") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ "targetReplicaId": "" }' \ + https://.cloud.appwrite.io/v1/compute/databases//ha/failovers +``` +{% /multicode %} + +Omit `targetReplicaId` to let the platform pick the lowest-lag replica. + +# Read your writes {% #read-your-writes %} + +In `async` mode, a read that lands on a replica immediately after a write to the primary may not see the write yet. The application strategies: + +- **Always read from the primary**, the safest default. The pooler routes all queries to the primary if read/write splitting is off. +- **Use read replicas, with primary-affinity for writes**, turn on the [connection pooler with read/write split](/docs/products/databases/dedicated/pooler). The pooler pins transactions and writes to the primary and routes ambient `SELECT`s to replicas. +- **Use a stronger sync mode**, in `sync` or `quorum`, any reader on an in-sync replica sees the write as soon as `COMMIT` returns. + +# Cross-region failover {% #cross-region-failover %} + +{% info title="Coming soon" %} +Cross-region failover is not yet available. The API surface, pricing, and behaviour described below are previewed for context; the feature will ship in a future release. +{% /info %} + +Beyond same-region HA, you can also configure a cross-region warm standby. The standby lives in another region and continuously replicates from the primary. On a planned or unplanned region-level failure you promote the standby and your DNS hostname starts resolving to the standby region. + +Cross-region replicas and standbys are billed as feature add-ons, see the [specifications page](/docs/products/databases/dedicated/specifications#add-ons) for the pricing. + +# Limits {% #limits %} + +| Limit | Value | +|------------------------------------------------|-------| +| Maximum replicas per database | 5 | +| Replica count change (online) | yes | +| Sync mode change without recreating replicas | yes | +| Failover cooldown after auto-promote | 60 s | +| Health-check timeout per attempt | 3 s | +| Consecutive failed health checks before failover | 2 | + +Regional HA replicas are billed at 50% of the underlying specification price per replica. Cross-region replicas are billed at 75% per replica. diff --git a/src/routes/docs/products/databases/dedicated/monitoring/+page.markdoc b/src/routes/docs/products/databases/dedicated/monitoring/+page.markdoc new file mode 100644 index 0000000000..efbc603e28 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/monitoring/+page.markdoc @@ -0,0 +1,924 @@ +--- +layout: article +title: Monitoring & insights +description: Live CPU, memory, IOPS, and connection metrics, slow query logging, query plan explanations, and automatic index recommendations for dedicated databases. +--- + +Every dedicated database collects its own resource telemetry, slow-query logs, and index analysis. The Appwrite Console renders charts from the platform usage API. Platform activity is available through the activity events API, and lower-level engine diagnostics stay on database-specific surfaces. + +# Usage time series {% #usage %} + +For Console charts, longer-range capacity planning, anomaly detection, and billing-adjacent views, use the platform usage endpoints. Gauge queries support `metric` and `time` filters. Dedicated database gauges scope the database inside the metric name, such as `dedicatedDatabases..cpu`. + +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + --get \ + --data-urlencode 'queries[]=equal("metric", ["dedicatedDatabases..cpu"])' \ + --data-urlencode 'queries[]=greaterThanEqual("time", ["2026-05-01T00:00:00.000Z"])' \ + https://.cloud.appwrite.io/v1/usage/gauges +``` + +Gauge metrics include `storage`, `connections`, `cpu`, `memory`, `qps`, `iopsRead`, and `iopsWrite`. Gauge rows also include `resourceType` and `resourceId` when they are ingested through the dedicated database metrics pipeline. Counter metrics include `compute`, `inbound`, `outbound`, and `coldStarts`. + +Use `/v1/usage/events` when you need discrete usage events, such as storage autoscaling, failover, or lifecycle transitions: + +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + --get \ + --data-urlencode 'queries[]=equal("resource", ["dedicatedDatabases"])' \ + --data-urlencode 'queries[]=equal("resourceId", [""])' \ + --data-urlencode 'queries[]=equal("metric", ["dedicatedDatabases..coldStarts"])' \ + --data-urlencode 'queries[]=greaterThanEqual("time", ["2026-05-01T00:00:00.000Z"])' \ + https://.cloud.appwrite.io/v1/usage/events +``` + +Console charts and per-database rollups should be built from these usage streams. + +# Live operational snapshot {% #metrics %} + +Use the dedicated metrics endpoint only when you need a short, live operational snapshot for the database view or for direct troubleshooting. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const metrics = await compute.getDatabaseMetrics({ + databaseId: '', + period: '24h', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const metrics = await compute.getDatabaseMetrics({ + databaseId: '', + period: '24h', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$metrics = $compute->getDatabaseMetrics( + databaseId: '', + period: '24h', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +metrics = compute.get_database_metrics( + database_id='', + period='24h', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +metrics = compute.get_database_metrics( + database_id: '', + period: '24h', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var metrics = await compute.GetDatabaseMetrics( + databaseId: "", + period: "24h" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final metrics = await compute.getDatabaseMetrics( + databaseId: '', + period: '24h', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val metrics = compute.getDatabaseMetrics( + databaseId = "", + period = "24h", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let metrics = try await compute.getDatabaseMetrics( + databaseId: "", + period: "24h" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + metrics, err := compute.GetDatabaseMetrics("", + compute.WithGetDatabaseMetricsPeriod("24h"), + ) + if err != nil { + panic(err) + } + _ = metrics +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let metrics = compute.get_database_metrics("") + .period("24h") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + "https://.cloud.appwrite.io/v1/compute/databases//metrics?period=24h" +``` +{% /multicode %} + +`period` accepts `1h`, `24h`, `7d`, or `30d`. + +The response includes: + +| Field | Unit | Notes | +|----------------------|---------------|----------------------------------------------------| +| `cpuPercent` | % | CPU utilisation over the period | +| `memoryPercent` | % | RSS as % of the specification's memory budget | +| `memoryUsedBytes` | bytes | Resident memory | +| `storageUsedBytes` | bytes | Engine on-disk size (data + indexes) | +| `connectionsActive` | count | Currently active backend connections | +| `connectionsMax` | count | Specification's connection cap | +| `iopsRead` | operations/s | Disk read IOPS | +| `iopsWrite` | operations/s | Disk write IOPS | +| `qps` | queries/s | Queries per second (across all sessions) | + +Metrics are sampled inside the database environment from OS-level resource counters and engine-native stats. Samples are pushed to the edge every 60 seconds and aggregated for the API. + +# Slow queries {% #slow-queries %} + +Each engine logs queries that run longer than the configured threshold to the slow-query log. The threshold defaults to 200 ms and is tunable via `metricsSlowQueryLogThresholdMs`. List slow queries through: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const queries = await compute.listDatabaseQueries({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const queries = await compute.listDatabaseQueries({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$queries = $compute->listDatabaseQueries(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +queries = compute.list_database_queries(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +queries = compute.list_database_queries(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var queries = await compute.ListDatabaseQueries(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final queries = await compute.listDatabaseQueries( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val queries = compute.listDatabaseQueries( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let queries = try await compute.listDatabaseQueries( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + queries, err := compute.ListDatabaseQueries("") + if err != nil { + panic(err) + } + _ = queries +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let queries = compute.list_database_queries("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//slow-queries +``` +{% /multicode %} + +The response includes the query text, average duration, call count, user, and the matching database. The Appwrite Console deduplicates by query shape so you see the worst offenders first. + +# Query plan explanation {% #explanation %} + +To inspect the planner's choice for a specific query without running it against your production database, post the query to the explanation endpoint: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const explanation = await compute.createDatabaseQueryExplanation({ + databaseId: '', + query: 'SELECT * FROM users WHERE email = $1', + analyze: false, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const explanation = await compute.createDatabaseQueryExplanation({ + databaseId: '', + query: 'SELECT * FROM users WHERE email = $1', + analyze: false, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$explanation = $compute->createDatabaseQueryExplanation( + databaseId: '', + query: 'SELECT * FROM users WHERE email = $1', + analyze: false, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +explanation = compute.create_database_query_explanation( + database_id='', + query='SELECT * FROM users WHERE email = $1', + analyze=False, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +explanation = compute.create_database_query_explanation( + database_id: '', + query: 'SELECT * FROM users WHERE email = $1', + analyze: false, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var explanation = await compute.CreateDatabaseQueryExplanation( + databaseId: "", + query: "SELECT * FROM users WHERE email = $1", + analyze: false +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final explanation = await compute.createDatabaseQueryExplanation( + databaseId: '', + query: 'SELECT * FROM users WHERE email = \$1', + analyze: false, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val explanation = compute.createDatabaseQueryExplanation( + databaseId = "", + query = "SELECT * FROM users WHERE email = \$1", + analyze = false, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let explanation = try await compute.createDatabaseQueryExplanation( + databaseId: "", + query: "SELECT * FROM users WHERE email = $1", + analyze: false +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + explanation, err := compute.CreateDatabaseQueryExplanation("", + compute.WithCreateDatabaseQueryExplanationQuery("SELECT * FROM users WHERE email = $1"), + compute.WithCreateDatabaseQueryExplanationAnalyze(false), + ) + if err != nil { + panic(err) + } + _ = explanation +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let explanation = compute.create_database_query_explanation("") + .query("SELECT * FROM users WHERE email = $1") + .analyze(false) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "query": "SELECT * FROM users WHERE email = $1", + "analyze": false + }' \ + https://.cloud.appwrite.io/v1/compute/databases//explanation +``` +{% /multicode %} + +| Engine | Underlying command | +|------------|---------------------------------------------------------------| +| PostgreSQL | `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ...` when `analyze` | +| MySQL | `EXPLAIN FORMAT=JSON ...` | +| MariaDB | `EXPLAIN FORMAT=JSON ...` | +| MongoDB | `db.collection.find(...).explain("executionStats")` | + +With `analyze: false`, the planner returns its estimated plan without touching the table. With `analyze: true`, the query is executed and you get actual timings, buffer usage, and row counts. The response includes both a parsed structured plan and the raw planner output. + +# Insights {% #insights %} + +In addition to metrics and slow queries, the platform runs a periodic index-analysis pass against each database (every ~24 hours, jittered) and submits the results as **insights**. Insights surface: + +- **Missing indexes**, tables with sequential scans on large row counts +- **Unused indexes**, indexes that have not been read since the last reset +- **Tuning recommendations**, engine config parameters that look out of range for the current specification + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const insights = await compute.getDatabaseInsights({ + databaseId: '', + period: '24h', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const insights = await compute.getDatabaseInsights({ + databaseId: '', + period: '24h', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$insights = $compute->getDatabaseInsights( + databaseId: '', + period: '24h', +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +insights = compute.get_database_insights( + database_id='', + period='24h', +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +insights = compute.get_database_insights( + database_id: '', + period: '24h', +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var insights = await compute.GetDatabaseInsights( + databaseId: "", + period: "24h" +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final insights = await compute.getDatabaseInsights( + databaseId: '', + period: '24h', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val insights = compute.getDatabaseInsights( + databaseId = "", + period = "24h", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let insights = try await compute.getDatabaseInsights( + databaseId: "", + period: "24h" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + insights, err := compute.GetDatabaseInsights("", + compute.WithGetDatabaseInsightsPeriod("24h"), + ) + if err != nil { + panic(err) + } + _ = insights +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let insights = compute.get_database_insights("") + .period("24h") + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + "https://.cloud.appwrite.io/v1/compute/databases//insights?period=24h" +``` +{% /multicode %} + +`period` accepts `1h`, `24h`, or `7d`. + +Insights are recommendations, not auto-applied changes. The Console suggests the corresponding `CREATE INDEX` or `DROP INDEX` statement; you apply it (or not) through your normal migration process. + +# Audit logs {% #audit %} + +Platform-level admin activity, such as credential rotation, IP allowlist changes, backup triggers, and failover requests, is available through the standard activity events API: + +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + --get \ + --data-urlencode 'queries[]=equal("resourceType", ["dedicatedDatabase"])' \ + --data-urlencode 'queries[]=equal("resourceId", [""])' \ + https://.cloud.appwrite.io/v1/activities/events +``` + +These activity events describe Appwrite platform actions. Activity events are not a replacement for engine audit logs, engine error logs, or slow-query logs. Engine logs remain lower-level database surfaces exposed through the engine's own logging and profiling tools, plus the dedicated slow-query view documented above. + +# Webhooks & realtime {% #events %} + +Every database lifecycle transition emits an event that fans out through Appwrite's standard event system. Subscribe with webhooks, realtime channels, or function triggers to react in your own systems: + +| Event | When | +|------------------------------------------------|-----------------------------------------------| +| `databases..create` | Provisioning completed | +| `databases..update` | Any update applied (resize, HA, backups, etc.)| +| `databases..delete` | Database removed | +| `databases..backups..create` | A backup completed (scheduled or manual) | +| `databases..restore` | A restore (full or PITR) completed | + +Lifecycle events emitted by the edge, `spindown`, `storage.autoscaled`, `failover.completed`, also fan out through this same channel. + +# What's not exposed {% #out-of-band %} + +A few things deliberately stay inside the database environment and are not surfaced through the API: + +- **Engine error logs** beyond what's in slow-query and audit feeds. For deeper engine logs, ship your own logging extension (`pgaudit`, `log_min_duration_statement`, MongoDB profiler). +- **OS-level metrics** beyond CPU/memory/IOPS. For per-process inspection, use the engine's own introspection (`pg_stat_activity`, `SHOW PROCESSLIST`, `db.currentOp()`). +- **Backup file contents**, the platform never exposes raw dump bytes through the API, only metadata. diff --git a/src/routes/docs/products/databases/dedicated/network/+page.markdoc b/src/routes/docs/products/databases/dedicated/network/+page.markdoc new file mode 100644 index 0000000000..da81edaf85 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/network/+page.markdoc @@ -0,0 +1,714 @@ +--- +layout: article +title: Network +description: Per-database hostname, mandatory TLS, optional IP allowlists, and connection limits for dedicated databases. +--- + +Every dedicated database gets a public hostname behind Appwrite's edge proxy. The proxy terminates TLS, applies your IP allowlist, and forwards the connection to the database engine over the Appwrite network. + +# Hostname {% #hostname %} + +On provision, a database hostname is generated in the form: + +``` +db--..appwrite.network +``` + +Example: `db-proj456-abc123.fra.appwrite.network`. + +The hostname is stable for the lifetime of the database. It resolves to the regional edge proxy via a wildcard `*.appwrite.network` DNS record, so you do not need to manage DNS yourself. + +# TLS {% #tls %} + +TLS is enabled on every dedicated database endpoint and is enforced by the edge proxy. Connection strings always include the engine's TLS flag: + +| Engine | Driver flag | +|------------|-------------------------------| +| PostgreSQL | `sslmode=require` | +| MySQL | `useSSL=true` / `tls=true` | +| MariaDB | `useSSL=true` / `tls=true` | +| MongoDB | `tls=true` | + +The certificate is signed by a well-known public CA, your driver does not need a custom CA bundle. + +To require mutual TLS (mTLS) where the client presents a certificate signed by your CA, contact support. mTLS is configured at the edge proxy and enforced before the connection is forwarded. + +# IP allowlist {% #allowlist %} + +By default, a dedicated database is reachable from any IP. To restrict access, set the `networkIPAllowlist` field with a list of CIDR ranges: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkIPAllowlist: [ + '203.0.113.0/24', + '198.51.100.10/32', + ], +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkIPAllowlist: [ + '203.0.113.0/24', + '198.51.100.10/32', + ], +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + networkIPAllowlist: [ + '203.0.113.0/24', + '198.51.100.10/32', + ], +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + network_ip_allowlist=[ + '203.0.113.0/24', + '198.51.100.10/32', + ], +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + network_ip_allowlist: [ + '203.0.113.0/24', + '198.51.100.10/32', + ], +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + networkIPAllowlist: new List { + "203.0.113.0/24", + "198.51.100.10/32" + } +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + networkIPAllowlist: [ + '203.0.113.0/24', + '198.51.100.10/32', + ], +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + networkIPAllowlist = listOf( + "203.0.113.0/24", + "198.51.100.10/32", + ), +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + networkIPAllowlist: [ + "203.0.113.0/24", + "198.51.100.10/32", + ] +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseNetworkIPAllowlist([]string{ + "203.0.113.0/24", + "198.51.100.10/32", + }), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .network_ip_allowlist(vec![ + "203.0.113.0/24".to_string(), + "198.51.100.10/32".to_string(), + ]) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "networkIPAllowlist": [ + "203.0.113.0/24", + "198.51.100.10/32" + ] + }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +The allowlist is enforced at the edge proxy. A connection from a non-allowlisted IP is dropped at TCP handshake, no banner, no error message. Up to 100 CIDR entries are accepted per database. + +Send an empty array to remove all entries and restore unrestricted access. + +# Max connections {% #connections %} + +Each specification has a hard ceiling on the number of simultaneous connections (see the [specifications page](/docs/products/databases/dedicated/specifications)). Use `networkMaxConnections` to set a per-database limit at or below that ceiling: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkMaxConnections: 500, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkMaxConnections: 500, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + networkMaxConnections: 500, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + network_max_connections=500, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + network_max_connections: 500, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + networkMaxConnections: 500 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + networkMaxConnections: 500, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + networkMaxConnections = 500, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + networkMaxConnections: 500 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseNetworkMaxConnections(500), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .network_max_connections(500) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ "networkMaxConnections": 500 }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +When the limit is reached, new connection attempts are rejected at the proxy with a clear error rather than being queued or held open. + +The pooler (if enabled) is counted as a single backend connection per pooled slot, not per client, turning on the pooler is the right way to push effective client concurrency well above this limit. + +# Connection idle timeout {% #idle-timeout %} + +Idle connections are closed by the edge proxy after `networkIdleTimeoutSeconds` of inactivity. The default is 900 (15 minutes). A higher value keeps connection-heavy clients alive longer; a lower value reclaims unused slots faster. + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkIdleTimeoutSeconds: 600, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + networkIdleTimeoutSeconds: 600, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + networkIdleTimeoutSeconds: 600, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + network_idle_timeout_seconds=600, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + network_idle_timeout_seconds: 600, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + networkIdleTimeoutSeconds: 600 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + networkIdleTimeoutSeconds: 600, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + networkIdleTimeoutSeconds = 600, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + networkIdleTimeoutSeconds: 600 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseNetworkIdleTimeoutSeconds(600), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .network_idle_timeout_seconds(600) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ "networkIdleTimeoutSeconds": 600 }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +# Locking down access {% #lockdown %} + +By default, a dedicated database is reachable over the public internet on the engine's standard port. To lock the surface down: + +1. **Restrict the allowlist** to your application server egress IPs only. +2. **Rotate the primary password** through the credentials endpoint after onboarding, so the initial generated password doesn't live in CI logs or chat history forever. +3. **Keep TLS on**, the platform enforces it at the proxy, so non-TLS clients can't connect even if they tried. +4. **Avoid embedding credentials in client-side code.** Connect from a server-side workload, an Appwrite Function, your application backend, or a CI job, and surface results through your own API. + +For workloads that should never be reachable from the public internet at all, contact support to discuss private endpoint options. + +# Limits {% #limits %} + +| Limit | Value | +|------------------------------------|--------------------------------| +| IP allowlist entries | up to 100 CIDR ranges | +| Max simultaneous connections (cap) | 10,000 (specification-bound) | +| Idle timeout range | 60 – 86,400 seconds (24 hours) | diff --git a/src/routes/docs/products/databases/dedicated/pooler/+page.markdoc b/src/routes/docs/products/databases/dedicated/pooler/+page.markdoc new file mode 100644 index 0000000000..83089c1d48 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/pooler/+page.markdoc @@ -0,0 +1,716 @@ +--- +layout: article +title: Connection pooler +description: Optional per-database connection pooler that multiplexes many client connections onto a small backend pool. Includes pool modes and read/write splitting when HA is enabled. +--- + +The connection pooler is an opt-in component that runs alongside your database and multiplexes a large number of client connections onto a small number of backend connections. Use it to absorb traffic spikes, smooth out connection churn from serverless runtimes, and route read-only queries to HA replicas without changing your application code. + +Each dedicated database can run its own pooler. Pooler resources are billed as part of the underlying specification, there's no separate pooler line item. + +# Engine support {% #engines %} + +| Engine | Pooler | Port | Notes | +|------------|---------|-------|-------------------------------------------------------------| +| PostgreSQL | yes | 6432 | PgBouncer-compatible wire protocol, native read/write split | +| MySQL | yes | 6033 | Query-rule based routing of reads vs writes | +| MariaDB | yes | 6033 | Same as MySQL | +| MongoDB | no | - | Not applicable, use the driver's `readPreference` setting | + +The pooler exposes its own client-facing port on the database endpoint. Clients connect to the pooler port instead of the engine port, everything else is the same. + +# Pool modes {% #modes %} + +PostgreSQL and MySQL/MariaDB pools support two modes: + +| Mode | Backend connection lifetime | Best for | +|----------------|-------------------------------------------|---------------------------------------------------------------------| +| `transaction` | Held until `COMMIT` / `ROLLBACK` | Most workloads. The default. Massive client→backend amplification. | +| `session` | Held for the whole client session | Apps that rely on `SET LOCAL`, prepared statements, advisory locks | + +If your application uses features that are tied to a single backend connection, server-side prepared statements (PostgreSQL), advisory locks, temporary tables, `SET LOCAL`, listen/notify, pick `session`. Otherwise, `transaction` is the right choice. + +# Enable the pooler {% #enable %} + +The pooler is enabled by default when a paid dedicated database is provisioned. To disable it or change its settings, use the pooler endpoint: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabasePooler({ + databaseId: '', + enabled: true, + poolMode: 'transaction', + maxClientConn: 200, + defaultPoolSize: 25, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabasePooler({ + databaseId: '', + enabled: true, + poolMode: 'transaction', + maxClientConn: 200, + defaultPoolSize: 25, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabasePooler( + databaseId: '', + enabled: true, + poolMode: 'transaction', + maxClientConn: 200, + defaultPoolSize: 25, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database_pooler( + database_id='', + enabled=True, + pool_mode='transaction', + max_client_conn=200, + default_pool_size=25, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database_pooler( + database_id: '', + enabled: true, + pool_mode: 'transaction', + max_client_conn: 200, + default_pool_size: 25, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabasePooler( + databaseId: "", + enabled: true, + poolMode: "transaction", + maxClientConn: 200, + defaultPoolSize: 25 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabasePooler( + databaseId: '', + enabled: true, + poolMode: 'transaction', + maxClientConn: 200, + defaultPoolSize: 25, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabasePooler( + databaseId = "", + enabled = true, + poolMode = "transaction", + maxClientConn = 200, + defaultPoolSize = 25, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabasePooler( + databaseId: "", + enabled: true, + poolMode: "transaction", + maxClientConn: 200, + defaultPoolSize: 25 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabasePooler("", + compute.WithUpdateDatabasePoolerEnabled(true), + compute.WithUpdateDatabasePoolerPoolMode("transaction"), + compute.WithUpdateDatabasePoolerMaxClientConn(200), + compute.WithUpdateDatabasePoolerDefaultPoolSize(25), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database_pooler("") + .enabled(true) + .pool_mode("transaction") + .max_client_conn(200) + .default_pool_size(25) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "enabled": true, + "poolMode": "transaction", + "maxClientConn": 200, + "defaultPoolSize": 25 + }' \ + https://.cloud.appwrite.io/v1/compute/databases//pooler +``` +{% /multicode %} + +Or to read the current configuration: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const pooler = await compute.getDatabasePooler({ + databaseId: '', +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const pooler = await compute.getDatabasePooler({ + databaseId: '', +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$pooler = $compute->getDatabasePooler(databaseId: ''); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +pooler = compute.get_database_pooler(database_id='') +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +pooler = compute.get_database_pooler(database_id: '') +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var pooler = await compute.GetDatabasePooler(databaseId: ""); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final pooler = await compute.getDatabasePooler( + databaseId: '', +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val pooler = compute.getDatabasePooler( + databaseId = "", +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let pooler = try await compute.getDatabasePooler( + databaseId: "" +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + pooler, err := compute.GetDatabasePooler("") + if err != nil { + panic(err) + } + _ = pooler +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + let pooler = compute.get_database_pooler("").await?; + + Ok(()) +} +``` +```bash +curl -X GET \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + https://.cloud.appwrite.io/v1/compute/databases//pooler +``` +{% /multicode %} + +Default settings: + +| Field | Default | Notes | +|----------------------|--------------------------|-----------------------------------------------------------------------| +| `enabled` | `true` on create | Disable to turn off the pooler entirely | +| `poolMode` | `transaction` | Pool mode at the pooler layer | +| `maxClientConn` | 100 | Max simultaneous client connections to the pooler | +| `defaultPoolSize` | 25 | Backend connections per (user, db) pair | +| `readWriteSplitting` | `true` if HA is enabled | Route ambient `SELECT`s to replicas | + +# Connect to the pooler {% #connect %} + +Same hostname, different port: + +```bash +psql -h db--..appwrite.network -p 6432 -U appwrite -d appwrite + +mysql -h db--..appwrite.network -P 6033 -u appwrite -p appwrite +``` + +The pooler accepts the same TLS, authentication, and parameter syntax as the engine. Your application driver doesn't know it's talking to a pooler. + +# Read/write splitting {% #read-write %} + +When the database has HA enabled and `readWriteSplitting` is on, the pooler routes queries based on type: + +**MySQL / MariaDB:** + +- `INSERT`, `UPDATE`, `DELETE`, DDL → primary +- `SELECT ... FOR UPDATE` / `FOR SHARE` → primary +- `SELECT` outside a transaction → replicas +- Everything inside a `BEGIN`…`COMMIT` block → pinned to the primary + +**PostgreSQL:** + +- The pooler's query parser inspects each statement. +- `INSERT`, `UPDATE`, `DELETE`, DDL, function calls → primary. +- `SELECT` outside a transaction → replicas. +- Anything inside a transaction → primary. + +This gives you read scale on aggregations and dashboard queries while keeping write-heavy transactions on the primary, with no SDK changes. + +Disable read/write splitting if your workload requires every read to see the most recent write, in async replication mode there's a small lag window where a read on a replica may not yet see a just-committed write: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabasePooler({ + databaseId: '', + readWriteSplitting: false, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabasePooler({ + databaseId: '', + readWriteSplitting: false, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabasePooler( + databaseId: '', + readWriteSplitting: false, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database_pooler( + database_id='', + read_write_splitting=False, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database_pooler( + database_id: '', + read_write_splitting: false, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabasePooler( + databaseId: "", + readWriteSplitting: false +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabasePooler( + databaseId: '', + readWriteSplitting: false, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabasePooler( + databaseId = "", + readWriteSplitting = false, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabasePooler( + databaseId: "", + readWriteSplitting: false +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabasePooler("", + compute.WithUpdateDatabasePoolerReadWriteSplitting(false), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database_pooler("") + .read_write_splitting(false) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ "readWriteSplitting": false }' \ + https://.cloud.appwrite.io/v1/compute/databases//pooler +``` +{% /multicode %} + +# Sizing {% #sizing %} + +A pooler is most useful when client connections outnumber backend connections by at least 4–5×. For a database that handles a small, stable number of long-lived application connections, the pooler adds latency without much benefit. + +Rule of thumb: + +- **`defaultPoolSize`** ≈ `min(specification.maxConnections, 4 × CPU cores)`. Going above 4×cores rarely improves throughput on PostgreSQL. +- **`maxClientConn`** = your peak client connection count. Serverless platforms can spike to thousands of clients; pick a generous value. + +# When not to use the pooler {% #anti-patterns %} + +- **Long-lived listen/notify subscribers** (PostgreSQL), these need a session-bound connection. Use `session` mode or bypass the pooler. +- **MongoDB workloads**, MongoDB doesn't have a pooler equivalent in this catalog; rely on the driver's connection pool and `readPreference: 'secondary'` for replica reads. +- **Single-tenant, single-process apps**, the engine's own connection handling is enough; the pooler is overhead. + +For everything else, Lambda, Cloud Run, serverless GraphQL backends, edge runtimes that open a new connection per request, turn it on. diff --git a/src/routes/docs/products/databases/dedicated/specifications/+page.markdoc b/src/routes/docs/products/databases/dedicated/specifications/+page.markdoc new file mode 100644 index 0000000000..9e0f3d92a8 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/specifications/+page.markdoc @@ -0,0 +1,107 @@ +--- +layout: article +title: Specifications +description: Eight dedicated database specifications from 1 vCPU and 1 GB to 8 vCPU and 64 GB, plus a free shared tier. Per-specification CPU, memory, included storage, included bandwidth, connection limits, and pricing. +--- + +A dedicated database is provisioned against a **specification**, a fixed bundle of CPU, memory, included storage, included bandwidth, and a connection limit. You pick the specification on create and can change it later by updating the database; the platform performs a blue-green resize with logical replication. + +All paid specifications are dedicated, always-on databases billed by the hour. The free specification is a shared, scale-to-zero database included with the Pro plan. + +# Paid specifications {% #paid %} + +| Specification | Name | vCPU | Memory | Included storage | Included bandwidth | Price (USD/mo) | +|------------------|------------------|------|--------|------------------|--------------------|----------------| +| `s-1vcpu-1gb` | Starter | 1 | 1 GB | 10 GB | 50 GB | $13 | +| `s-2vcpu-2gb` | Standard | 2 | 2 GB | 25 GB | 200 GB | $23 | +| `s-2vcpu-4gb` | Standard Plus | 2 | 4 GB | 50 GB | 400 GB | $49 | +| `s-4vcpu-8gb` | Professional | 4 | 8 GB | 200 GB | 1 TB | $85 | +| `s-4vcpu-16gb` | Business | 4 | 16 GB | 500 GB | 2 TB | $160 | +| `s-4vcpu-32gb` | Business Plus | 4 | 32 GB | 1 TB | 5 TB | $299 | +| `s-8vcpu-32gb` | Enterprise | 8 | 32 GB | 2 TB | 7.5 TB | $425 | +| `s-8vcpu-64gb` | Enterprise Plus | 8 | 64 GB | 3 TB | 10 TB | $699 | + +vCPU counts are dedicated, not burstable. Paid specifications are always-on and billed by the hour against the monthly price. + +# Connection limits {% #connections %} + +Connection caps are enforced at the engine layer. Going above the cap returns a connection refusal, not a slowdown. + +| Specification | Max connections | +|------------------|-----------------| +| Free shared | 10 | +| `s-1vcpu-1gb` | 100 | +| `s-2vcpu-2gb` | 200 | +| `s-2vcpu-4gb` | 500 | +| `s-4vcpu-8gb` | 1,000 | +| `s-4vcpu-16gb` | 2,000 | +| `s-4vcpu-32gb` | 4,000 | +| `s-8vcpu-32gb` | 5,000 | +| `s-8vcpu-64gb` | 10,000 | + +{% arrow_link href="/contact-us/enterprise" %} +Contact sales for custom connection limits +{% /arrow_link %} + +# Free (shared) specification {% #free %} + +The free specification is shared compute with strict resource caps. Idle databases scale to zero and the underlying disk is preserved between spin-downs, so you only ever pay for the storage you keep, never for the database container itself. + +| Resource | Limit | +|------------------|-------------| +| CPU | 0.125 vCPU | +| Memory | 128 MB | +| Storage | 1 GB | +| Max connections | 10 | +| Query timeout | 15 seconds | +| Idle timeout | 15 minutes | + +Free databases are included with the Pro plan and do not count toward storage or bandwidth overage. + +# Overage rates {% #overage %} + +When usage exceeds the included allowance for a specification, overage is billed at the following rates: + +| Resource | Rate | +|-----------|-----------------| +| Storage | $0.125 / GB / month | +| Bandwidth | $0.08 / GB / month | + +Storage overage is computed on the snapshot at the end of the billing period. Bandwidth overage sums inbound and outbound traffic across the billing period. + +# Feature add-ons {% #add-ons %} + +The following features are billed as a percentage of the underlying specification price, pro-rated by active days in the billing period: + +| Feature | Rate | Example on an $85/mo specification | +|----------------------------------------|---------------------------------------|------------------------------------| +| High availability replicas | 50% of specification per replica | $42.50/mo per replica | +| Cross-region replicas *(coming soon)* | 75% of specification per replica | $63.75/mo per replica | +| Point-in-time recovery | 20% of specification | $17/mo | +| PostgreSQL extensions | Free | $0 | + +{% info title="Cross-region replication is coming soon" %} +The cross-region replica and cross-region standby that back the [cross-region failover](/docs/products/databases/dedicated/high-availability#cross-region-failover) feature are previewed here for context; they will ship in a future release. The standby is a cross-region replica configured for automatic failover and will bill the same way as any other cross-region replica. +{% /info %} + +# Resize behaviour {% #resize %} + +Changing the specification on an existing database is a non-destructive, online operation. There are no read or write outages during the resize. + +1. A second database instance is provisioned with the new CPU and memory. +2. The schema and data are streamed from the old instance to the new one through engine-native logical replication, WAL on PostgreSQL, binlog on MySQL/MariaDB, and oplog tailing on MongoDB. +3. Once replication is caught up, the connection endpoint is switched to the new instance. +4. The old instance is decommissioned. + +Storage can only grow, never shrink, storage volumes can be expanded but not contracted. Storage expansion happens online without restarting the database. + +# Picking a specification {% #picking %} + +A few rules of thumb: + +- **OLTP workloads** (lots of small reads and writes), pick a specification where the working set fits in memory. For PostgreSQL this is `shared_buffers + effective_cache_size` ≈ 75% of RAM. +- **Analytics workloads** (large scans, aggregations), favour the higher CPU specifications. `s-8vcpu-32gb` and `s-8vcpu-64gb` are tuned for sustained CPU usage. +- **Storage-heavy workloads**, pick by included storage first. Going one specification up is usually cheaper than paying overage on a smaller specification. +- **Burst patterns**, keep autoscaling on for storage and use a specification one tier below your peak; resize up during peak windows and back down after. + +The next page explains how to [connect to a dedicated database](/docs/products/databases/dedicated/connect). diff --git a/src/routes/docs/products/databases/dedicated/sql-api/+page.markdoc b/src/routes/docs/products/databases/dedicated/sql-api/+page.markdoc new file mode 100644 index 0000000000..8e10991dbb --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/sql-api/+page.markdoc @@ -0,0 +1,586 @@ +--- +layout: article +title: SQL API +description: Execute parameterised SQL against a dedicated database over HTTPS without opening a TCP connection. Useful from edge runtimes, serverless functions, and CI scripts. +--- + +The **SQL API** is a managed HTTP endpoint that executes one parameterised SQL statement against a dedicated database and returns the result as JSON. It runs alongside the database itself, so it sees the same latency as a local connection, but you call it from anywhere a TCP database connection isn't practical (edge runtimes, serverless functions on cold starts, CI scripts, internal admin tools). + +The SQL API is opt-in per database and supports all four engines. + +# Enable the SQL API {% #enable %} + +The SQL API is disabled by default. Enable it through the database update endpoint: + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + sqlApiEnabled: true, + sqlApiAllowedStatements: ['SELECT'], + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30, +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +await compute.updateDatabase({ + databaseId: '', + sqlApiEnabled: true, + sqlApiAllowedStatements: ['SELECT'], + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30, +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$compute->updateDatabase( + databaseId: '', + sqlApiEnabled: true, + sqlApiAllowedStatements: ['SELECT'], + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30, +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +compute.update_database( + database_id='', + sql_api_enabled=True, + sql_api_allowed_statements=['SELECT'], + sql_api_max_rows=10000, + sql_api_max_bytes=10485760, + sql_api_timeout_seconds=30, +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +compute.update_database( + database_id: '', + sql_api_enabled: true, + sql_api_allowed_statements: ['SELECT'], + sql_api_max_rows: 10000, + sql_api_max_bytes: 10485760, + sql_api_timeout_seconds: 30, +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +await compute.UpdateDatabase( + databaseId: "", + sqlApiEnabled: true, + sqlApiAllowedStatements: new List { "SELECT" }, + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30 +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +await compute.updateDatabase( + databaseId: '', + sqlApiEnabled: true, + sqlApiAllowedStatements: ['SELECT'], + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30, +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +compute.updateDatabase( + databaseId = "", + sqlApiEnabled = true, + sqlApiAllowedStatements = listOf("SELECT"), + sqlApiMaxRows = 10000, + sqlApiMaxBytes = 10485760, + sqlApiTimeoutSeconds = 30, +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +_ = try await compute.updateDatabase( + databaseId: "", + sqlApiEnabled: true, + sqlApiAllowedStatements: ["SELECT"], + sqlApiMaxRows: 10000, + sqlApiMaxBytes: 10485760, + sqlApiTimeoutSeconds: 30 +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + _, err := compute.UpdateDatabase("", + compute.WithUpdateDatabaseSqlApiEnabled(true), + compute.WithUpdateDatabaseSqlApiAllowedStatements([]string{"SELECT"}), + compute.WithUpdateDatabaseSqlApiMaxRows(10000), + compute.WithUpdateDatabaseSqlApiMaxBytes(10485760), + compute.WithUpdateDatabaseSqlApiTimeoutSeconds(30), + ) + if err != nil { + panic(err) + } +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.update_database("") + .sql_api_enabled(true) + .sql_api_allowed_statements(vec!["SELECT".to_string()]) + .sql_api_max_rows(10000) + .sql_api_max_bytes(10485760) + .sql_api_timeout_seconds(30) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X PATCH \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "sqlApiEnabled": true, + "sqlApiAllowedStatements": ["SELECT"], + "sqlApiMaxRows": 10000, + "sqlApiMaxBytes": 10485760, + "sqlApiTimeoutSeconds": 30 + }' \ + https://.cloud.appwrite.io/v1/compute/databases/ +``` +{% /multicode %} + +The defaults above are safe to start with: `SELECT`-only, 10k rows, 10 MB payload, 30-second timeout. + +# Configuration {% #config %} + +| Field | Default | Plan ceiling | Purpose | +|---------------------------|-----------------------|-------------------------------------|--------------------------------------------------------| +| `sqlApiEnabled` | `false` | n/a | Master switch | +| `sqlApiAllowedStatements` | `["SELECT"]` | `[SELECT, INSERT, UPDATE, DELETE]` | Statement types accepted | +| `sqlApiMaxRows` | 10,000 | 1,000,000 | Hard row cap; truncates beyond | +| `sqlApiMaxBytes` | 10 MB | 100 MB | Hard payload cap; truncates beyond | +| `sqlApiTimeoutSeconds` | 30 | 300 (5 min) | Server-side cancellation deadline | + +Only `SELECT`, `INSERT`, `UPDATE`, and `DELETE` are ever accepted. Schema-altering statements (`CREATE`, `DROP`, `TRUNCATE`, `ALTER`, `GRANT`, `REVOKE`) are rejected unconditionally, those must go through the dedicated provisioning APIs or a direct database connection. + +# Execute a statement {% #execute %} + +{% multicode %} +```server-nodejs +import { Client, Compute } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const execution = await compute.createDatabaseExecution({ + databaseId: '', + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 50], +}); +``` +```server-deno +import { Client, Compute } from "npm:node-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const compute = new Compute(client); + +const execution = await compute.createDatabaseExecution({ + databaseId: '', + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 50], +}); +``` +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$compute = new Compute($client); + +$execution = $compute->createDatabaseExecution( + databaseId: '', + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 50], +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.compute import Compute + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +compute = Compute(client) + +execution = compute.create_database_execution( + database_id='', + sql='SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings=['2026-05-01T00:00:00Z', 50], +) +``` +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +compute = Compute.new(client) + +execution = compute.create_database_execution( + database_id: '', + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 50], +) +``` +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Compute compute = new Compute(client); + +var execution = await compute.CreateDatabaseExecution( + databaseId: "", + sql: "SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2", + bindings: new List { "2026-05-01T00:00:00Z", 50 } +); +``` +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Compute compute = Compute(client); + +final execution = await compute.createDatabaseExecution( + databaseId: '', + sql: 'SELECT id, email FROM users WHERE created_at > \$1 ORDER BY created_at DESC LIMIT \$2', + bindings: ['2026-05-01T00:00:00Z', 50], +); +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Compute + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val compute = Compute(client) + +val execution = compute.createDatabaseExecution( + databaseId = "", + sql = "SELECT id, email FROM users WHERE created_at > \$1 ORDER BY created_at DESC LIMIT \$2", + bindings = listOf("2026-05-01T00:00:00Z", 50), +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let compute = Compute(client) + +let execution = try await compute.createDatabaseExecution( + databaseId: "", + sql: "SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2", + bindings: ["2026-05-01T00:00:00Z", 50] +) +``` +```server-go +package main + +import ( + "github.com/appwrite/sdk-for-go/appwrite" +) + +func main() { + client := appwrite.NewClient( + appwrite.WithEndpoint("https://.cloud.appwrite.io/v1"), + appwrite.WithProject(""), + appwrite.WithKey(""), + ) + + compute := appwrite.NewCompute(client) + + execution, err := compute.CreateDatabaseExecution("", + compute.WithCreateDatabaseExecutionSql("SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2"), + compute.WithCreateDatabaseExecutionBindings([]interface{}{"2026-05-01T00:00:00Z", 50}), + ) + if err != nil { + panic(err) + } + _ = execution +} +``` +```server-rust +use appwrite::Client; +use appwrite::services::compute::Compute; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let compute = Compute::new(&client); + + compute.create_database_execution("") + .sql("SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2") + .bindings(serde_json::json!(["2026-05-01T00:00:00Z", 50])) + .send() + .await?; + + Ok(()) +} +``` +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "sql": "SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2", + "bindings": ["2026-05-01T00:00:00Z", 50] + }' \ + https://.cloud.appwrite.io/v1/compute/databases//execution +``` +{% /multicode %} + +Response: + +```json +{ + "rows": [ + { "id": "...", "email": "..." }, + ... + ], + "rowCount": 50, + "truncated": false, + "durationMs": 14, + "columns": [ + { "name": "id", "type": "uuid" }, + { "name": "email", "type": "text" } + ] +} +``` + +If the result exceeds `sqlApiMaxRows` or `sqlApiMaxBytes`, the response sets `truncated: true` and includes whatever fits. If the statement exceeds `sqlApiTimeoutSeconds`, the server cancels it and returns `dedicated_database_sql_api_timeout`. + +# Parameter binding {% #bindings %} + +Bindings are always sent through a separate field, Appwrite never interpolates them into the SQL string. Two styles are accepted: + +**Positional**, list of values, referenced as `$1`, `$2`, … in the SQL. + +```json +{ + "sql": "SELECT * FROM events WHERE user_id = $1 AND created_at > $2", + "bindings": ["usr_abc", "2026-05-01"] +} +``` + +**Named**, a `name => value` map, referenced as `:name` or `@name` depending on engine convention. + +```json +{ + "sql": "SELECT * FROM events WHERE user_id = :user AND created_at > :since", + "bindings": { "user": "usr_abc", "since": "2026-05-01" } +} +``` + +Pick one style per request, mixing positional and named bindings in the same statement is rejected. + +# Per-database whitelist {% #whitelist %} + +The platform always rejects DDL and privilege statements, but you can narrow the accepted DML further per database. For example, a read-only dashboard would set: + +```json +{ "sqlApiAllowedStatements": ["SELECT"] } +``` + +…and a writer-only ingestion endpoint: + +```json +{ "sqlApiAllowedStatements": ["INSERT"] } +``` + +The whitelist is enforced before the request leaves Appwrite, so a disallowed statement never touches the database. + +# Errors {% #errors %} + +| Code | When | +|-----------------------------------------------------|-------------------------------------------------------| +| `dedicated_database_sql_api_disabled` | API not enabled on the database | +| `dedicated_database_sql_api_statement_not_allowed` | Statement type not in the per-DB whitelist | +| `dedicated_database_sql_api_parse_failed` | SQL did not parse | +| `dedicated_database_sql_api_multiple_statements` | Submission contained more than one statement | +| `dedicated_database_sql_api_result_too_large` | Rows or bytes exceed the limit (returned as 200 with `truncated: true` if soft-truncating, 400 if rejecting outright) | +| `dedicated_database_sql_api_timeout` | Statement exceeded `sqlApiTimeoutSeconds` | +| `dedicated_database_sql_api_binding_invalid` | Binding count, name, or type mismatch | +| `dedicated_database_sql_api_sidecar_unavailable` | Per-database SQL API runtime is not ready | + +# When to use the SQL API {% #use-cases %} + +The SQL API trades raw throughput for portability. Use it when: + +- **Calling from an edge runtime** that can't open a long-lived TCP connection, Cloudflare Workers, Vercel Edge Functions, Deno Deploy. +- **Building an admin tool** that runs ad-hoc queries from a UI without shipping engine drivers to the browser. +- **Writing a one-off CI script**, easier to `curl` than to install `psql` and manage credentials in CI. + +Don't use it when: + +- **Throughput matters**, TCP + a connection pool is faster end-to-end. The SQL API adds an HTTP hop. +- **The query is transactional**, each SQL API call is its own statement. There's no `BEGIN`…`COMMIT` window across calls. + +# Audit {% #audit %} + +Every SQL API call produces an audit event of type `dedicatedDatabase.createExecution`, persisted with the user ID, project ID, database ID, statement keyword, duration, and result size. The full SQL text is **not** stored by default, but the statement type and binding count are. Enable verbose audit logging on the database settings if you need the full text in your audit log.