diff --git a/.optimize-cache.json b/.optimize-cache.json index e52ed7770aa..1445abde09d 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -1749,9 +1749,11 @@ "static/images/integrations/avatars/appsignal.png": "69e569f5c89b86a073f4f6ad8c2c33119a1de5e7991353d8be345c11bcdf32c0", "static/images/integrations/avatars/aws.png": "7306ef5be26cf0d3fc0d9a50512828af4e69d6c7d53b835c17982d7db554cb65", "static/images/integrations/avatars/claude.png": "4cf4f4c37eb4c306bade50c50529bb8d00a16b5b888693190c2e2988f3bbf8b8", + "static/images/integrations/avatars/cloudflare.webp": "f7d4cbbb53f7dcf160dbc6aca2b15c1359b2ba57c613a0adf17548263b94982f", "static/images/integrations/avatars/cursor.png": "2d78f88bf6d0793dc40f98061077e9e5220ff4f521c07aa8274585c25806b3bf", "static/images/integrations/avatars/discord.png": "d938bfc4096a6f50c4dee4360d48b0b3df63ea5eb3830049f11053aac15bd4d4", "static/images/integrations/avatars/docusaurus.png": "01ed9efa99e62547546c9725cd96a08c8d866535d00a75e49fed0e00acee8a1e", + "static/images/integrations/avatars/drizzle.webp": "73c605d6f5ed645b648f0e62fc99c989461a7410e82bd83e36523ff4dafccd1d", "static/images/integrations/avatars/elevenlabs.png": "ffec825739c2058c2cd592f503b6c0d6eff6f925546e438a6efa8fb8b1a3ca79", "static/images/integrations/avatars/fcm.png": "21b184d127efc726724693fc090d1780d1554fec8841ac76c494a88cc94ffb4a", "static/images/integrations/avatars/flutterflow.png": "b7dc391b3dd8b0c2a12f42c0d366472a1be90d53a46ba4fdd00df63f724b3a7e", @@ -1761,10 +1763,12 @@ "static/images/integrations/avatars/lemon-squeezy.png": "d76c0c04b8c0518de03882c10c601fabe77c4e30fdb3fade928b8cfff2b6c1f2", "static/images/integrations/avatars/magic-portfolio.png": "fac810df4bd0e62618cb1295588289f13197bac4805c8232211335523f9812c2", "static/images/integrations/avatars/mongodb.png": "16fb2fcfaf3a8e1c1f007c81b1bccdfb2463f971c641f1e3a54278c4b7e07513", + "static/images/integrations/avatars/nextjs.webp": "67a56a84d1b5356da9d00543c55021e90c6086f4943de3e61f8a78219492fe5e", "static/images/integrations/avatars/notion.png": "04f581fd7f20af3351e808c1714d66b8cb78546913e6de42c3ecaf51042297d8", "static/images/integrations/avatars/nxtlnk.png": "ff3b1907ab894fb8fa204a418325fa62789f4bd1b46b876f488ef9c284889f0e", "static/images/integrations/avatars/openai.png": "8e2a950f55a705708a71c4a813046050e5a199f1098db20562fac93591e6d3f1", "static/images/integrations/avatars/perplexity.png": "1aeff5375655bf397465f7824c9baffbfc867669a80dafc475091c55b17c199b", + "static/images/integrations/avatars/prisma.webp": "f460e2402149a3595016b8155ddf1ff2697cc6b964899d6e324ada8ec479030f", "static/images/integrations/avatars/raygun.png": "dffa2cf7b4e6717b9fa578c22dd1f9e919952fb32ee76de8a0addc052fb4f183", "static/images/integrations/avatars/react-admin.png": "8e89fed781a54d8a5dbfa9116f8dea0d83dfc273a482d28bd627ec32517642a9", "static/images/integrations/avatars/rxdb.png": "b46c8cef0d75139add85308998ff3f27379f080df0afa8dbeb48a49155be6b9d", @@ -1774,13 +1778,16 @@ "static/images/integrations/avatars/stripe.png": "d6a0919aff3e53e2f55022749daa63e5dbe6b5c7ea095a0d72e2ad5006246ae0", "static/images/integrations/avatars/twilio.png": "35ee999626e2179cd643eb89a3e1b425894b420da9fda8656239ca20d2fedec9", "static/images/integrations/avatars/upstash.png": "c404ea0c7f2d2bc28bdb37e6019f8ac9d706d3b7f0b7de2d3e1c0b90dc4b10a9", + "static/images/integrations/avatars/vercel.webp": "65e0db1f1b67f35d7b6774faf21ebabf822ad831d71d85f7a77541cc1f39266d", "static/images/integrations/avatars/vonage.png": "b9d22975cae0fc7234c761b8ea5d92260db73cc027e756ede9aa47cc2efde44e", "static/images/integrations/avatars/vuepress.png": "7893861c4fdb3bc037381962aa4ada05156d8df276d5300925c8fd8d66063e23", "static/images/integrations/avatars/windsurf.png": "41d48db8811ebe2c5bffb965615ede43bee55ef73cb751db4ffc7499e576033c", "static/images/integrations/avatars/x.png": "cd24d39505021b078939ca05fba4ddb5d0e67afedc2a02c29837b097abb1904e", + "static/images/integrations/cloudflare/cover.webp": "c15c819302251d8c20d6e43de656efbd8f6b8b0f7a57718f9b61580e26fc50d7", "static/images/integrations/deployments-github/cover.png": "c425b990a458a660eec087677bf4ce81cc9a188115654ef56bed4791f6c03d03", "static/images/integrations/deployments-github/create.png": "64477b19f98d50a3648fc9b5e2587a45a694297480277e2806dd30c60b7f7e67", "static/images/integrations/deployments-github/installed.png": "14cb46d5dcca35a8df915c571505a6198adba52afb24968a75a248fcfdb416f4", + "static/images/integrations/drizzle/cover.webp": "8b65fb51134d4dd77c029aec34e9ecd0284a8d33145919d6fa19158a90d80377", "static/images/integrations/email-sendgrid/cover.png": "3822b48eb800ee55ceb2a44077fdc68e35caf2a7229b0dc51223f49489d9c926", "static/images/integrations/email-sendgrid/email.png": "1de5d9bea2d47030eef44a5c1581ff6ea5a2ee8851e3410f3a41a6b7017d6cdf", "static/images/integrations/email-sendgrid/provider.png": "d37ab3b9783701d6395991fc6fee7af8a843cc1dee4d851b0295ae8370a94700", @@ -1830,6 +1837,7 @@ "static/images/integrations/native-auth-apple/cover.png": "da94b788046c69191e794b5c9007588a4cf908b17af7bc77775258f43f91c13d", "static/images/integrations/native-auth-apple/template.png": "7e9f1105cadd93f09041e77f7b09ed4c40779b0f1a5a9a6829e400f6d42e5750", "static/images/integrations/native-auth-apple/variables.png": "798727e10a77a2fe0596f0321d18997903e48bff5f36af042c8707fbf43a5c24", + "static/images/integrations/nextjs/cover.webp": "d6002b444349090db0afe3eea0db4d543bef7b5e466ce98405d214876dad531d", "static/images/integrations/oauth-amazon/allowed-return-urls.png": "b092ffd113714231193d10b39fb46835b1e921b20aac6c981ec94d95e5ab3b47", "static/images/integrations/oauth-amazon/cover.png": "893075c608b921b697e8a8769d8e821a1a68123fa2831ad650f32d253daf0d25", "static/images/integrations/oauth-amazon/provider.png": "3e481065acd9924c4ac8df860c4e9b27fa12a27fdaa7c68dc35f6176b06d7aab", @@ -1857,6 +1865,7 @@ "static/images/integrations/phone-auth-twilio/cover.png": "e46ab3a1a8b458b69e919219e35de4a15b2bbd0ea9c0079c0c99e005acf3a295", "static/images/integrations/phone-auth-twilio/twilio-console.png": "bd3081b13711088c437ed10553709ca4af54f80c81571b30b6cc9a4a1fab1799", "static/images/integrations/phone-auth-twilio/user.png": "a526a9621c30de2ba4b6b08508d15bcf588b7de5cafadaf3ea7a8198485cf53e", + "static/images/integrations/prisma/cover.webp": "041de3db19330b14f0c15c7213a76970c9bc9676c4aad3caee81b6aeca9e8ae9", "static/images/integrations/push-apns/apple-developer-program.png": "2f13f017496e5e2497a32485840e0d8302df6933ed8159a7bb5031e02cb98562", "static/images/integrations/push-apns/cover.png": "da94b788046c69191e794b5c9007588a4cf908b17af7bc77775258f43f91c13d", "static/images/integrations/push-apns/provider.png": "16332791662d979d11880e229e15a9356ddcd5488ac48bb7e7b90eed04c1e10b", @@ -1962,6 +1971,7 @@ "static/images/integrations/stripe-subscriptions/web-platform.png": "4fa7e4ef19d6417f49d651deaf62e158173aec5da2d21e150de679bfc25163b0", "static/images/integrations/stripe-subscriptions/webhooks.png": "641cc545aa64d137619a7768c553f9aeb30507cd7209cf33bc08f476b95975eb", "static/images/integrations/terraform/cover.png": "43b4f901f490adab9f018c600882132192c29092f44154226fc72196e2d5a3fe", + "static/images/integrations/vercel/cover.webp": "964169c15ef8a689db6b41edea0d0a2db4a87364c87fb3af06822a25aee3106b", "static/images/integrations/whatsapp-vonage/cover.png": "c445579cca51fcafa1a0717abf51386e1d86a5909951a7a05401b0e77dc506d4", "static/images/integrations/whatsapp-vonage/demo.png": "34ffa1310f6d01e2c22b0d4473d8f89f4306610d8db1d01f53771da6628023e6", "static/images/integrations/whatsapp-vonage/settings.png": "57dc4bb24aac093bdd115c60d350de19a14d87fc5ed08d8bf833889a95414570", diff --git a/src/lib/utils/code.ts b/src/lib/utils/code.ts index 87503c1066a..9f74c6a6347 100644 --- a/src/lib/utils/code.ts +++ b/src/lib/utils/code.ts @@ -111,8 +111,11 @@ Object.entries(platformAliases).forEach(([key, value]) => { }); }); -/** HashiCorp Configuration Language (Terraform); core highlight.js has no HCL grammar */ -hljs.registerAliases(['hcl', 'terraform', 'tf'], { languageName: 'ini' }); +/** HashiCorp Configuration Language (Terraform) and TOML; core highlight.js has no grammar for either, INI is the closest match */ +hljs.registerAliases(['hcl', 'terraform', 'tf', 'toml'], { languageName: 'ini' }); + +/** Prisma schema language; core highlight.js has no Prisma grammar, GraphQL is the closest match */ +hljs.registerAliases(['prisma'], { languageName: 'graphql' }); export type Language = keyof typeof languages | Platform; diff --git a/src/routes/docs/products/databases/+layout.svelte b/src/routes/docs/products/databases/+layout.svelte index ec407d28aa5..56f046a6e83 100644 --- a/src/routes/docs/products/databases/+layout.svelte +++ b/src/routes/docs/products/databases/+layout.svelte @@ -187,6 +187,116 @@ } ] }, + { + label: 'Connect your stack', + items: [ + { + label: 'Node.js drivers', + href: '/docs/products/databases/dedicated/drivers', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Prisma', + href: '/docs/products/databases/dedicated/prisma', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Drizzle', + href: '/docs/products/databases/dedicated/drizzle', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Auth.js', + href: '/docs/products/databases/dedicated/auth-js', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Better Auth', + href: '/docs/products/databases/dedicated/better-auth', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Laravel', + href: '/docs/products/databases/dedicated/laravel', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Rails', + href: '/docs/products/databases/dedicated/rails', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Django', + href: '/docs/products/databases/dedicated/django', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'FastAPI', + href: '/docs/products/databases/dedicated/fastapi', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Spring Boot', + href: '/docs/products/databases/dedicated/spring-boot', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'EF Core', + href: '/docs/products/databases/dedicated/ef-core', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'GORM', + href: '/docs/products/databases/dedicated/gorm', + new: isNewUntil('31 Aug 2026') + } + ] + }, + { + label: 'Deploy', + items: [ + { + label: 'Next.js', + href: '/docs/products/databases/dedicated/nextjs', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Vercel', + href: '/docs/products/databases/dedicated/vercel', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Cloudflare', + href: '/docs/products/databases/dedicated/cloudflare', + new: isNewUntil('31 Aug 2026') + } + ] + }, + { + label: 'Analytics & BI', + items: [ + { + label: 'Metabase', + href: '/docs/products/databases/dedicated/metabase', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Grafana', + href: '/docs/products/databases/dedicated/grafana', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'Retool', + href: '/docs/products/databases/dedicated/retool', + new: isNewUntil('31 Aug 2026') + }, + { + label: 'dbt', + href: '/docs/products/databases/dedicated/dbt', + new: isNewUntil('31 Aug 2026') + } + ] + }, { label: 'References', items: [ diff --git a/src/routes/docs/products/databases/dedicated/auth-js/+page.markdoc b/src/routes/docs/products/databases/dedicated/auth-js/+page.markdoc new file mode 100644 index 00000000000..dd262430052 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/auth-js/+page.markdoc @@ -0,0 +1,234 @@ +--- +layout: article +title: Auth.js +description: Use an Appwrite dedicated database as the backing store for Auth.js (NextAuth.js). Persist users, accounts, and sessions through a Prisma or Drizzle adapter, pooled from serverless runtimes. +--- + +[Auth.js](https://authjs.dev/) (formerly NextAuth.js) persists users, accounts, sessions, and verification tokens through a database adapter. When you configure an adapter, those records live in your own database instead of only in a cookie, which is what makes database sessions, account linking, and email sign-in possible. An Appwrite [dedicated database](/docs/products/databases/dedicated) is a standard PostgreSQL, MySQL, or MariaDB engine, so any Auth.js adapter built on a regular SQL ORM works against it with no Appwrite-specific configuration. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Choose an adapter {% #adapter %} + +Auth.js doesn't talk to the database directly, it goes through an official adapter. For a dedicated database, use whichever ORM you already run: + +- **Prisma** through [`@auth/prisma-adapter`](https://authjs.dev/getting-started/adapters/prisma). See the [Prisma guide](/docs/products/databases/dedicated/prisma) for the full datasource, pooling, and migration setup. +- **Drizzle** through [`@auth/drizzle-adapter`](https://authjs.dev/getting-started/adapters/drizzle). See the [Drizzle guide](/docs/products/databases/dedicated/drizzle) for the driver connection and `drizzle-kit` migration setup. + +The connection details, ports, and pooling behaviour are the same for both. The rest of this page shows the Prisma path and notes the Drizzle equivalents. + +# Set the connection string {% #connection-string %} + +Auth.js adapters read the database URL from the environment, exactly like any other ORM workload. Copy the connection string from the Console **Connect** view or the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials), and give the adapter two URLs: a **pooled** one for the running app and a **direct** one for migrations. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +# Runtime: pooled, transaction mode (prepared statements off) +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require&pgbouncer=true" + +# Migrations: direct connection to the engine +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +Prisma's `mysql` provider drives MariaDB, keep the `mysql://` scheme. +{% /tabsitem %} +{% /tabs %} + +The app runtime connects on the [pooler](/docs/products/databases/dedicated/pooler) port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB), and the schema migration runs over the direct engine port (`5432` / `3306`). The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns, the edge proxy terminates TLS, so no certificate setup is needed. For full certificate verification (`verify-full`) or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Create the adapter schema {% #schema %} + +Auth.js ships a [canonical schema](https://authjs.dev/getting-started/database#models) of four core models, `User`, `Account`, `Session`, and `VerificationToken`. With Prisma, add them to `prisma/schema.prisma` and point the datasource at both URLs: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} +{% /tabs %} + +The generator and models are engine-agnostic: + +```prisma +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] +} + +model Account { + userId String + provider String + providerAccountId String + access_token String? + expires_at Int? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([provider, providerAccountId]) +} + +model Session { + sessionToken String @id + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model VerificationToken { + identifier String + token String + expires DateTime + + @@id([identifier, token]) +} +``` + +With Drizzle, define the equivalent tables in your schema file and generate a `drizzle-kit` migration instead. The table shapes are the same, the official adapter docs include ready-made PostgreSQL, MySQL, and SQLite schemas. + +# Run the migration over the direct endpoint {% #migrate %} + +Create the tables before the app serves any traffic. Prisma Migrate connects over `directUrl` (the engine port), so it gets a real session connection with full DDL privileges: + +```bash +npx prisma migrate dev --name authjs-init +``` + +In CI or production, apply already-generated migrations without prompting: + +```bash +npx prisma migrate deploy +``` + +With Drizzle, run `drizzle-kit push` (or `migrate()`) against the same direct URL. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL, so always migrate as `appwrite`. + +# Wire the adapter into Auth.js {% #config %} + +Pass the adapter to your Auth.js config through the `adapter` key. The adapter uses the Prisma Client (which reads the pooled `DATABASE_URL`) for every runtime read and write: + +```ts +import NextAuth from 'next-auth'; +import { PrismaAdapter } from '@auth/prisma-adapter'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export const { handlers, auth, signIn, signOut } = NextAuth({ + adapter: PrismaAdapter(prisma), + providers: [ + // your providers, e.g. GitHub, Google, Resend + ], +}); +``` + +The Drizzle equivalent is identical apart from the import, `adapter: DrizzleAdapter(db)` from `@auth/drizzle-adapter`, where `db` is your Drizzle instance. + +# Database vs JWT sessions {% #sessions %} + +Auth.js has two session strategies, and the adapter changes the default: + +- **`database`** — the default *once an adapter is configured*. A session row is written to the `Session` table and only an opaque session ID is stored in an `HttpOnly` cookie; each request looks the session up in the dedicated database. Sessions can be revoked server-side. +- **`jwt`** — the default when no adapter is set. Session state lives entirely in a signed cookie and the database is never read on the session path. + +You can set it explicitly: + +```ts +export const { handlers, auth } = NextAuth({ + adapter: PrismaAdapter(prisma), + session: { strategy: 'database' }, + providers: [], +}); +``` + +Even with `strategy: 'jwt'`, the adapter still persists users and linked accounts, so account linking and the admin view of users keep working; only the per-request session read moves off the database. + +# Pooling note {% #pooling %} + +On serverless and edge platforms (Vercel, Netlify, Cloudflare) every invocation is a fresh instance, so connecting straight to the engine fans out into more backend connections than it allows. Routing through the pooler in **transaction mode** (the default) absorbs that churn. Auth.js tables are write-light and read-heavy, exactly the access pattern transaction-mode pooling handles best, so the pooled `DATABASE_URL` above is the right default for the app runtime. + +Transaction mode does not hold a backend connection across statements, so server-side prepared statements are unavailable, which is why the PostgreSQL Prisma URL carries `pgbouncer=true` (and Drizzle's `postgres.js` client should be created with `prepare: false`). Migrations always use the direct endpoint, so they keep a full session connection. If you need session-bound features, switch the pooler to **session mode**, see the [pooler modes](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string, ideal for a pull-request preview or an integration-test job that signs users in and out against throwaway data: + +1. Create a branch from the API and read its `connectionString`. +2. Export the direct variant as `DIRECT_URL` and the pooled variant as `DATABASE_URL`. +3. Run `prisma migrate deploy` (or `drizzle-kit push`) and your auth flow against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the Auth.js tables already exist with realistic data, so preview sign-ins behave like production without touching it. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/prisma" title="Prisma" %} +Datasource, pooled and direct URLs, and migrations for the Prisma adapter. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/drizzle" title="Drizzle" %} +Driver connection, pooler settings, and drizzle-kit migrations per engine. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Transaction vs session mode, ports, and serverless connection handling. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/better-auth" title="Better Auth" %} +The same pattern for Better Auth on a dedicated database. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/better-auth/+page.markdoc b/src/routes/docs/products/databases/dedicated/better-auth/+page.markdoc new file mode 100644 index 00000000000..0a82599c057 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/better-auth/+page.markdoc @@ -0,0 +1,239 @@ +--- +layout: article +title: Better Auth +description: Use an Appwrite dedicated database as the database for Better Auth. Point the runtime at the pooler, run schema generation and migrations over the direct endpoint, and store users and sessions in your own PostgreSQL, MySQL, or MariaDB. +--- + +[Better Auth](https://www.better-auth.com/) is a framework-agnostic authentication library for TypeScript that keeps all of its state, users, sessions, accounts, and verification tokens, in a database you own. A dedicated database is a standard engine, so Better Auth works against it with no Appwrite-specific configuration: you give it the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and run the Better Auth CLI to create the schema. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Set the connection strings {% #connection-strings %} + +Better Auth needs two endpoints on the same database: a **pooled** one for your application runtime and a **direct** one for schema work. Copy the connection string from the Console **Connect** view, or fetch it from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials), and put both in your environment. Never commit them. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +# Runtime: pooler port (transaction mode), absorbs serverless connection churn +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require" + +# Schema generation & migrations: direct connection to the engine +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +# Runtime: pooler port (transaction mode), absorbs serverless connection churn +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Schema generation & migrations: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +# Runtime: pooler port (transaction mode), absorbs serverless connection churn +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Schema generation & migrations: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MariaDB uses the same `mysql://` scheme and ports as MySQL. +{% /tabsitem %} +{% /tabs %} + +The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns. The edge proxy terminates TLS for every dedicated database, so no extra certificate configuration is needed. For full certificate verification or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +The pooler runs on port `6432` for PostgreSQL (`6033` for MySQL/MariaDB) and the engine on `5432` (`3306` for MySQL/MariaDB). See [Connect](/docs/products/databases/dedicated/connect) for every port and the [connection pooler](/docs/products/databases/dedicated/pooler#modes) page for the pool modes. + +# Configure Better Auth {% #configure %} + +Better Auth accepts either a **direct database instance** (a driver connection pool, which it drives through its built-in Kysely adapter) or an **ORM adapter** (Prisma, Drizzle, Kysely). Both work against a dedicated database. + +## With a driver pool {% #pool %} + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +Pass a `pg` `Pool` pointed at the **pooler** endpoint so runtime traffic is multiplexed. Set `ssl: { rejectUnauthorized: true }` so the driver verifies the proxy's certificate: + +```ts +import { betterAuth } from 'better-auth'; +import { Pool } from 'pg'; + +export const auth = betterAuth({ + database: new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: { rejectUnauthorized: true } + }), + emailAndPassword: { enabled: true } +}); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +Better Auth's built-in adapter supports MySQL natively. Pass a `mysql2` pool pointed at the **pooler** endpoint so runtime traffic is multiplexed; TLS is already enabled by the `ssl=true` in the connection string, so no extra driver flags are needed: + +```ts +import { betterAuth } from 'better-auth'; +import { createPool } from 'mysql2/promise'; + +export const auth = betterAuth({ + database: createPool(process.env.DATABASE_URL), + emailAndPassword: { enabled: true } +}); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +Better Auth's MySQL path drives MariaDB the same way through the built-in Kysely adapter, pass the same `mysql2` pool: + +```ts +import { betterAuth } from 'better-auth'; +import { createPool } from 'mysql2/promise'; + +export const auth = betterAuth({ + database: createPool(process.env.DATABASE_URL), + emailAndPassword: { enabled: true } +}); +``` +{% /tabsitem %} +{% /tabs %} + +This is the simplest path: Better Auth manages the schema for you and can both generate and apply migrations through the CLI below. + +## With an ORM adapter {% #adapter %} + +If you already use an ORM, hand Better Auth an adapter instead of a raw `Pool`. Configure the ORM's own client against the dedicated database (pooled URL for the runtime client, direct URL for migrations), then wrap it: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from './db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { enabled: true } +}); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from './db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'mysql' }), + emailAndPassword: { enabled: true } +}); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ts +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from './db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'mysql' }), + emailAndPassword: { enabled: true } +}); +``` + +The adapter's `mysql` provider drives MariaDB the same way. +{% /tabsitem %} +{% /tabs %} + +Set the ORM up against the dedicated database first, then return here for the schema steps. The [Prisma](/docs/products/databases/dedicated/prisma) and [Drizzle](/docs/products/databases/dedicated/drizzle) guides cover the `datasource`/client config, the pooled-vs-direct URL split, and how transaction-mode pooling affects prepared statements. + +# Generate and migrate the schema {% #migrate %} + +The [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli) reads your `auth` config and creates the tables it needs. Run it with the **direct** endpoint so it gets a real session connection with DDL privileges, the pooler's transaction mode can't run schema changes reliably. + +Generate the schema for your setup. With a driver pool (built-in Kysely adapter) this produces a SQL file; with an ORM adapter it produces that ORM's schema (a Prisma schema, a Drizzle `schema.ts`): + +```bash +DATABASE_URL="$DIRECT_URL" npx @better-auth/cli@latest generate +``` + +If you're using the built-in adapter (a driver pool), apply the generated schema directly: + +```bash +DATABASE_URL="$DIRECT_URL" npx @better-auth/cli@latest migrate +``` + +`migrate` is only available for the built-in Kysely adapter. With a **Prisma** or **Drizzle** adapter, run `generate` to produce the schema, then apply it with that ORM's own migration tool, `prisma migrate deploy` or `drizzle-kit migrate`, again over `DIRECT_URL`. See the [Prisma](/docs/products/databases/dedicated/prisma#migrate) and [Drizzle](/docs/products/databases/dedicated/drizzle) guides for the exact commands. + +Both commands connect over the engine port, so they get full DDL privileges. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +# A minimal example {% #example %} + +After the schema exists, your runtime connects on the pooler URL and Better Auth handles the rest. Mount the handler for your framework and start creating users: + +```ts +import { auth } from './auth'; + +const result = await auth.api.signUpEmail({ + body: { + email: 'ada@example.com', + password: 'a-strong-password', + name: 'Ada Lovelace' + } +}); + +console.log(result.user.id); +``` + +On a long-running server, instantiate `auth` once and reuse it. On serverless, keep a single instance per module scope so warm invocations reuse it, and rely on the pooler to absorb cold-start connection churn. + +# Pooling and prepared statements {% #pooling %} + +Auth workloads, sign-up, login, session lookups, are short transactions, which is exactly what the pooler's default **transaction mode** is built for. Pointing your runtime pool at the pooler port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) lets a large number of serverless instances share a small backend pool. + +The one constraint of transaction mode is that it does not hold a backend connection across statements, so **server-side prepared statements are unavailable**. On PostgreSQL, the node-postgres `Pool` is fine with this out of the box. If you drive Better Auth through an ORM, disable prepared statements on the runtime client, with `postgres.js` set `prepare: false`; with Prisma on PostgreSQL add `pgbouncer=true` to the pooled URL. If your app genuinely needs prepared statements, advisory locks, or `LISTEN`/`NOTIFY`, switch the pooler to **session mode** instead, see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string, ideal for running auth migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Export it as `DIRECT_URL` (and the pooled variant as `DATABASE_URL`). +3. Run the Better Auth CLI (or your ORM migration) and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so auth flows run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/prisma" title="Prisma" %} +Drive the Better Auth Prisma adapter with pooled and direct URLs. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/drizzle" title="Drizzle" %} +Drive the Better Auth Drizzle adapter against a dedicated database. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/auth-js" title="Auth.js" %} +Use a dedicated database as the Auth.js (NextAuth) database. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/cloudflare/+page.markdoc b/src/routes/docs/products/databases/dedicated/cloudflare/+page.markdoc new file mode 100644 index 00000000000..dcd2b4f280d --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/cloudflare/+page.markdoc @@ -0,0 +1,309 @@ +--- +layout: article +title: Cloudflare +description: Connect to an Appwrite dedicated database from Cloudflare Workers and Pages, either over TCP through Hyperdrive or over HTTPS through the SQL API. +--- + +A dedicated database is a standard engine reachable over TLS, so Cloudflare's runtime can talk to it two different ways. Pick based on whether your Worker can hold a TCP connection and how much throughput you need. + +- **Hyperdrive (TCP).** [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) accelerates queries to an existing PostgreSQL or MySQL database by pooling connections at Cloudflare's edge and caching read queries. You give Hyperdrive your dedicated database's connection string, bind it to a Worker, and connect with a normal driver (`postgres.js`, `node-postgres`, `mysql2`, Drizzle). This is the highest-throughput option and the one to reach for when a real connection makes sense. +- **SQL API (HTTPS).** The [SQL API](/docs/products/databases/dedicated/sql-api) executes one parameterised statement over HTTPS and returns JSON. It needs nothing but `fetch`, no driver, no sockets, no binding, which makes it the simplest path for occasional reads, admin endpoints, or when you would rather not run Hyperdrive at all. + +Both paths work identically from **Workers** and from **Pages Functions**, since Pages runs on the same Workers runtime. + +Hyperdrive supports PostgreSQL and MySQL; the SQL API additionally supports MariaDB. MongoDB is not reachable through either path, connect to it with a MongoDB driver instead. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Path 1: Hyperdrive over TCP {% #hyperdrive %} + +Hyperdrive sits between your Worker and the database. It keeps a warm pool of backend connections, so each Worker invocation gets a connection instantly instead of paying for a fresh TLS and authentication handshake on every cold start. Under the hood the driver opens an outbound TCP socket using Cloudflare's [`connect()` API](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) from `cloudflare:sockets`; Hyperdrive manages that for you. + +## Create a Hyperdrive config {% #hyperdrive-config %} + +Point Hyperdrive at the **direct engine endpoint** (port `5432` for PostgreSQL, `3306` for MySQL), not the Appwrite [connection pooler](/docs/products/databases/dedicated/pooler). Hyperdrive does its own pooling, so layering it on top of the pooler would pool twice and break server-side prepared statements. Use the plain connection string from the [Connect](/docs/products/databases/dedicated/connect) page: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```bash +npx wrangler hyperdrive create appwrite-db \ + --connection-string="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```bash +npx wrangler hyperdrive create appwrite-db \ + --connection-string="mysql://appwrite:@db--..appwrite.center:3306/appwrite" +``` + +Drop the `ssl=true` parameter from the string Appwrite returns, Hyperdrive's MySQL connection strings take no query parameters and it always connects over TLS. +{% /tabsitem %} +{% /tabs %} + +Hyperdrive connects to the origin over TLS and validates the server certificate against public roots, so no extra certificate setup is needed. The command prints a Hyperdrive **id**, you'll reference it from the binding below. + +## Bind Hyperdrive to your Worker {% #hyperdrive-binding %} + +Add the binding to `wrangler.toml` and enable Node.js compatibility, which the database drivers require: + +```ini +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2026-06-05" +compatibility_flags = ["nodejs_compat"] + +[[hyperdrive]] +binding = "HYPERDRIVE" +id = "" +``` + +The binding surfaces in your Worker as `env.HYPERDRIVE`, exposing a `connectionString` you hand to any driver. + +## Query with postgres.js {% #hyperdrive-postgresjs %} + +Install `postgres` (3.4.5 or newer), then create a client per request. Because Hyperdrive owns the real pool, constructing a client is cheap, so do it inside the handler rather than at module scope: + +```ts +import postgres from 'postgres'; + +export interface Env { + HYPERDRIVE: Hyperdrive; +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const sql = postgres(env.HYPERDRIVE.connectionString, { + // Workers limit concurrent external connections; keep the per-isolate pool small. + max: 5, + // Skip the extra round trip to fetch array type metadata when you don't need it. + fetch_types: false, + }); + + const users = await sql` + SELECT id, email FROM users + WHERE created_at > ${'2026-05-01'} + ORDER BY created_at DESC + LIMIT 50 + `; + + // Flush the socket after the response is sent, without blocking it. + ctx.waitUntil(sql.end()); + + return Response.json(users); + }, +}; +``` + +## Query with node-postgres {% #hyperdrive-pg %} + +If you prefer `pg`, the pattern is the same, pass the Hyperdrive connection string to a `Client`: + +```ts +import { Client } from 'pg'; + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const client = new Client({ connectionString: env.HYPERDRIVE.connectionString }); + await client.connect(); + + const { rows } = await client.query( + 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + ['2026-05-01', 50], + ); + + ctx.waitUntil(client.end()); + return Response.json(rows); + }, +}; +``` + +## Query with mysql2 {% #hyperdrive-mysql2 %} + +For MySQL, use `mysql2` (3.13.0 or newer) and open a connection per request from the binding's credential fields. Set `disableEval: true`, the Workers runtime disallows `eval()`, which mysql2 otherwise uses when parsing results: + +```ts +import { createConnection } from 'mysql2/promise'; + +export interface Env { + HYPERDRIVE: Hyperdrive; +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const connection = await createConnection({ + host: env.HYPERDRIVE.host, + user: env.HYPERDRIVE.user, + password: env.HYPERDRIVE.password, + database: env.HYPERDRIVE.database, + port: env.HYPERDRIVE.port, + disableEval: true, + }); + + const [rows] = await connection.query( + 'SELECT id, email FROM users WHERE created_at > ? ORDER BY created_at DESC LIMIT ?', + ['2026-05-01', 50], + ); + + ctx.waitUntil(connection.end()); + return Response.json(rows); + }, +}; +``` + +## Query with Drizzle {% #hyperdrive-drizzle %} + +Drizzle wraps the same drivers. Point the engine's adapter at the Hyperdrive binding and use the typed query builder, see the [Drizzle](/docs/products/databases/dedicated/drizzle) page for schema setup: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { users } from './schema'; + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const client = postgres(env.HYPERDRIVE.connectionString, { max: 5, fetch_types: false }); + const db = drizzle(client); + + const rows = await db.select().from(users).limit(50); + + ctx.waitUntil(client.end()); + return Response.json(rows); + }, +}; +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +import { drizzle } from 'drizzle-orm/mysql2'; +import { createConnection } from 'mysql2/promise'; +import { users } from './schema'; + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const connection = await createConnection({ + host: env.HYPERDRIVE.host, + user: env.HYPERDRIVE.user, + password: env.HYPERDRIVE.password, + database: env.HYPERDRIVE.database, + port: env.HYPERDRIVE.port, + disableEval: true, + }); + const db = drizzle(connection); + + const rows = await db.select().from(users).limit(50); + + ctx.waitUntil(connection.end()); + return Response.json(rows); + }, +}; +``` +{% /tabsitem %} +{% /tabs %} + +# Path 2: SQL API over HTTPS {% #sql-api %} + +When you don't want to run Hyperdrive, or you only need a handful of reads, call the [SQL API](/docs/products/databases/dedicated/sql-api) with plain `fetch`. It's a managed HTTP endpoint that runs one parameterised statement and returns JSON. There's no driver, no socket, and no binding to configure, which suits edge code well. + +Enable the SQL API on the database first (see the [SQL API](/docs/products/databases/dedicated/sql-api#enable) page); it's opt-in and defaults to `SELECT`-only. Then `POST` to the execution endpoint from your Worker: + +```ts +export interface Env { + APPWRITE_PROJECT_ID: string; + APPWRITE_API_KEY: string; +} + +const REGION = 'fra'; // one of fra, nyc, sfo, sgp, syd, tor +const DATABASE_ID = ''; + +export default { + async fetch(request: Request, env: Env): Promise { + const response = await fetch( + `https://${REGION}.cloud.appwrite.io/v1/compute/databases/${DATABASE_ID}/execution`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Appwrite-Project': env.APPWRITE_PROJECT_ID, + 'X-Appwrite-Key': env.APPWRITE_API_KEY, + }, + body: JSON.stringify({ + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 50], + }), + }, + ); + + const result = await response.json(); + return Response.json(result); + }, +}; +``` + +Bindings travel in their own field, never interpolated into the SQL string, so the API is injection-safe by construction. The response carries the rows plus metadata: + +```json +{ + "rows": [{ "id": "...", "email": "..." }], + "rowCount": 50, + "truncated": false, + "durationMs": 14, + "columns": [ + { "name": "id", "type": "uuid" }, + { "name": "email", "type": "text" } + ] +} +``` + +# Secrets {% #secrets %} + +Never hard-code the database password or the Appwrite API key. Store them as Wrangler secrets so they're encrypted at rest and injected into `env` at runtime: + +```bash +# For the SQL API path +npx wrangler secret put APPWRITE_API_KEY +npx wrangler secret put APPWRITE_PROJECT_ID +``` + +The Hyperdrive path is even tighter: the database password lives only inside the Hyperdrive config you created with `wrangler hyperdrive create`, never in your Worker code or `wrangler.toml`. The Worker only ever sees `env.HYPERDRIVE.connectionString`. + +The Appwrite API key for the SQL API only needs the `databases.read` scope for `SELECT`s. Scope it down and [rotate the database password](/docs/products/databases/dedicated/connect#rotate) independently whenever you need to. + +# Pages {% #pages %} + +Cloudflare Pages runs [Pages Functions](https://developers.cloudflare.com/pages/functions/) on the same Workers runtime, so both paths apply unchanged. Declare the Hyperdrive binding and `nodejs_compat` flag in your Pages project (in the dashboard or `wrangler.toml`), then read `env.HYPERDRIVE` inside a function under `functions/`. For the SQL API, call `fetch` exactly as above and store the project ID and API key as Pages secrets. + +# Choosing a path {% #choosing %} + +| | Hyperdrive (TCP) | SQL API (HTTPS) | +|---|---|---| +| Throughput | Highest, pooled and cache-accelerated | Lower, one HTTP hop per statement | +| Setup | Hyperdrive config + binding + driver | None beyond enabling the API | +| Transactions | Full `BEGIN`…`COMMIT` support | One statement per call, no cross-call transaction | +| Statements | Any SQL the user is allowed to run | DML only, whitelisted per database | +| Engines | PostgreSQL, MySQL | PostgreSQL, MySQL, MariaDB | +| Best for | Application traffic, write paths, ORMs | Simple reads, admin tools, low-traffic endpoints | + +Use **Hyperdrive** for your application's main query path, anything write-heavy, transactional, or driven by an ORM. Use the **SQL API** when you want a dependency-free read from the edge, an internal dashboard query, or a quick endpoint where adding Hyperdrive isn't worth it. The two aren't exclusive, it's common to serve hot read paths through Hyperdrive and keep an occasional SQL API call for one-off lookups. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/sql-api" title="SQL API" %} +Enable the HTTP SQL endpoint, configure limits, and bind parameters. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/drizzle" title="Drizzle" %} +Set up the Drizzle schema and migrations against a dedicated database. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes and ports, and why Hyperdrive points at the direct endpoint. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/dbt/+page.markdoc b/src/routes/docs/products/databases/dedicated/dbt/+page.markdoc new file mode 100644 index 00000000000..0c0ae37bff4 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/dbt/+page.markdoc @@ -0,0 +1,129 @@ +--- +layout: article +title: dbt +description: Run dbt transformations against an Appwrite dedicated PostgreSQL database. Create a build user, configure profiles.yml for the direct endpoint, size threads to your connection budget, and test models against a branch in CI. +--- + +A dedicated database is a managed PostgreSQL (18 or 17) server, so [dbt](https://docs.getdbt.com/) works against it through the standard [`dbt-postgres`](https://docs.getdbt.com/docs/core/connect-data-platform/postgres-setup) adapter with no Appwrite-specific configuration. You point a `profiles.yml` target at the connection details from the [Connect](/docs/products/databases/dedicated/connect) page and use `dbt debug`, `dbt run`, and `dbt build` exactly as you would against any self-hosted PostgreSQL warehouse. + +dbt compiles your models into `CREATE TABLE` / `CREATE VIEW` statements and runs them in dependency order, materializing a transformed analytics layer inside a schema you control. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve them. The default database name and username are both `appwrite`, and the engine listens on port `5432`. +{% /info %} + +# Create a build user {% #build-user %} + +dbt issues DDL (`CREATE`, `DROP`, `ALTER`) to build your models, so the target it connects with needs ownership of the schema it writes to. The primary `appwrite` user owns the default database and can run DDL, so it is the simplest choice for the build connection. + +If you'd rather isolate dbt behind its own credentials, create a dedicated connection user from the API. Use the `readwrite` role and have dbt build into a schema that this user owns: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "dbt", + "database": "appwrite", + "role": "readwrite" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//connections +``` + +The response contains a generated password, copy it now, it is shown only once. See [Connect](/docs/products/databases/dedicated/connect#connections) for the full connection-user reference. Whichever user you choose, point it at a build schema (for example `analytics`) so transformed tables never collide with your source data. + +# Configure profiles.yml {% #profiles %} + +dbt reads connection details from `~/.dbt/profiles.yml` (or the project directory). Configure a `postgres` target against the dedicated database's host on port `5432`, and set `sslmode: require` so the connection is encrypted: + +```yaml +analytics: + target: dev + outputs: + dev: + type: postgres + host: db--..appwrite.center + port: 5432 + user: appwrite + password: "{{ env_var('APPWRITE_DB_PASSWORD') }}" + dbname: appwrite + schema: analytics + sslmode: require + threads: 4 +``` + +The dbt-postgres adapter uses `password` (not `pass`) and `dbname` (you may also write `database`). Read the password from an environment variable with `env_var` rather than committing it. The `schema` key is where dbt materializes models, set it to the build schema your user owns. + +The edge proxy terminates TLS for every dedicated database, so `sslmode: require` needs no extra certificate files. For full certificate verification, set `sslmode: verify-full` with `sslrootcert` pointing at a trusted root store, `system` on libpq 16+, or your OS bundle such as `/etc/ssl/certs/ca-certificates.crt`, the proxy's certificate is signed by a public CA, so there is no Appwrite-specific CA to download. + +# Test the connection {% #debug %} + +`dbt debug` validates your project files and opens a connection to confirm the credentials and host are correct: + +```bash +dbt debug +``` + +A successful run reports `Connection test: OK connection ok`. If it fails, recheck the host, port, `sslmode`, and password. + +# Run transformations {% #run %} + +Build your models into the analytics schema: + +```bash +dbt run +``` + +`dbt run` executes models only, materializing each as a table or view. Use `dbt build` to run models, tests, seeds, and snapshots together in DAG order, a failing test on an upstream model then skips its dependents: + +```bash +dbt build +``` + +# Choose the right endpoint {% #endpoint %} + +dbt opens one database connection per thread and uses each to run DDL and rely on session state (search paths, temporary objects, transactions spanning multiple statements). Point dbt at a connection that preserves that session: + +- **Direct endpoint (port `5432`)** is the simplest and recommended target. Each thread gets a real backend session with full DDL privileges. This is what the `profiles.yml` above uses. +- **Session-mode pooler** also works, because it holds a backend connection for the whole client session. Connect on the pooler port and switch the pool to `session` mode. + +Do **not** point dbt at the **transaction-mode** pooler (the pooler default). Transaction mode returns the backend connection to the pool after every statement, so the session state and multi-statement DDL that dbt depends on break. See the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the mode trade-offs. + +# Size threads to your connection budget {% #threads %} + +The `threads` setting controls how many models dbt builds in parallel, and dbt opens one connection per thread. A `threads: 8` run can hold up to eight backend connections at once. dbt also respects model dependencies, so it never runs more models concurrently than your DAG allows, regardless of the thread count. + +Pick a `threads` value that fits the connection budget for your database spec, and leave headroom for any application traffic sharing the same database. If you connect through a session-mode pooler, the same per-thread connections apply at the backend, so size against the pool, not the client side. Start at the adapter default of `4` and raise it only while connections stay within budget. + +# Test transformations against a branch {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. Because a branch starts from a storage snapshot, its schema and data match the source at branch time, so dbt models run against production-like data without touching production. That makes branches ideal for validating transformations in CI: + +1. Create a branch from the API and read its `connectionString`. +2. Set the branch host and credentials as the `profiles.yml` target (or export them as `env_var` values). +3. Run `dbt build` against the branch so models and tests execute on realistic data. +4. Delete the branch when the job finishes. + +This gives every pull request a clean, production-shaped warehouse to build against, with no risk to live analytics tables. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes and ports. Use session mode for dbt, never transaction mode. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for CI and preview environments. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, and IP allowlists. +{% /cards_item %} +{% /cards %} + +{% arrow_link href="https://docs.getdbt.com/docs/core/connect-data-platform/postgres-setup" %} +dbt-postgres adapter reference +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/dedicated/django/+page.markdoc b/src/routes/docs/products/databases/dedicated/django/+page.markdoc new file mode 100644 index 00000000000..e73510fcf0b --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/django/+page.markdoc @@ -0,0 +1,240 @@ +--- +layout: article +title: Django +description: Use Django's ORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure the DATABASES setting with TLS options, run migrations against the direct endpoint, and tune persistent connections for pooling. +--- + +A dedicated database is a standard engine, so Django's ORM works against it with no Appwrite-specific configuration. You point the `DATABASES` setting at the credentials from the [Connect](/docs/products/databases/dedicated/connect) page and use migrations, models, and the rest of Django exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. PostgreSQL is the recommended engine for dedicated databases. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection details. The default database name and username are both `appwrite`. +{% /info %} + +# Install a driver {% #driver %} + +Django talks to the engine through a driver: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +The latest [psycopg 3](https://www.psycopg.org/psycopg3/) is recommended; `psycopg2` also works but is likely to be deprecated in a future Django release. + +```bash +pip install "psycopg[binary]" +``` + +Both drivers use the same `ENGINE` value, `django.db.backends.postgresql`. Django picks whichever package is installed, so no settings change is needed to switch between them. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +[mysqlclient](https://pypi.org/project/mysqlclient/) is the native driver Django recommends for its MySQL backend: + +```bash +pip install mysqlclient +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +MariaDB uses the same driver as MySQL: + +```bash +pip install mysqlclient +``` +{% /tabsitem %} +{% /tabs %} + +# Configure DATABASES {% #databases %} + +In `settings.py`, point the `default` connection at your dedicated database and require TLS through `OPTIONS`. The edge proxy terminates TLS for every dedicated database, so requiring it needs no certificate files. Read every value from the environment so credentials never land in source control: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```python +import os + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_USER"], + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ["DB_HOST"], + "PORT": os.environ["DB_PORT"], + "OPTIONS": { + "sslmode": "require", + }, + } +} +``` + +The PostgreSQL backend forwards everything in `OPTIONS` straight to the driver's connection, so `sslmode` is honoured the same way `psql` honours it in the connection string. For full certificate verification, set `"sslmode": "verify-full"` with `"sslrootcert"` pointing at a trusted root store, `system` on libpq 16+, or your OS bundle such as `/etc/ssl/certs/ca-certificates.crt`, the proxy's certificate is signed by a public CA, so there is no Appwrite-specific CA to download. For mTLS client certificates, see the [Network](/docs/products/databases/dedicated/network) page. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```python +import os + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_USER"], + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ["DB_HOST"], + "PORT": os.environ["DB_PORT"], + "OPTIONS": { + "ssl_mode": "REQUIRED", + }, + } +} +``` + +The MySQL backend forwards everything in `OPTIONS` straight to `mysqlclient`, so `ssl_mode` is honoured the same way the `mysql` CLI honours `--ssl-mode`. MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS, so keep `ssl_mode` at `REQUIRED` or stricter. For full certificate verification, set `"ssl_mode": "VERIFY_IDENTITY"` with `"ssl": {"ca": ...}` pointing at your OS bundle such as `/etc/ssl/certs/ca-certificates.crt`, the proxy's certificate is signed by a public CA, so there is no Appwrite-specific CA to download. For mTLS client certificates, see the [Network](/docs/products/databases/dedicated/network) page. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```python +import os + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["DB_NAME"], + "USER": os.environ["DB_USER"], + "PASSWORD": os.environ["DB_PASSWORD"], + "HOST": os.environ["DB_HOST"], + "PORT": os.environ["DB_PORT"], + "OPTIONS": { + "ssl_mode": "REQUIRED", + }, + } +} +``` + +The settings match MySQL, Django's `django.db.backends.mysql` backend officially supports MariaDB. +{% /tabsitem %} +{% /tabs %} + +Populate the environment from the values on the Console **Connect** view or the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials): + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DB_NAME=appwrite +DB_USER=appwrite +DB_PASSWORD= +DB_HOST=db--..appwrite.center +DB_PORT=5432 +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DB_NAME=appwrite +DB_USER=appwrite +DB_PASSWORD= +DB_HOST=db--..appwrite.center +DB_PORT=3306 +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DB_NAME=appwrite +DB_USER=appwrite +DB_PASSWORD= +DB_HOST=db--..appwrite.center +DB_PORT=3306 +``` +{% /tabsitem %} +{% /tabs %} + +The engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB) is the **direct** endpoint. Keep migrations on this port, see [pooling](#pooling) below for when to add the pooler. + +# Run migrations {% #migrate %} + +Generate migrations from your models, then apply them against the direct endpoint: + +```bash +python manage.py makemigrations +python manage.py migrate +``` + +`migrate` needs a real session connection with DDL privileges. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. Always run `migrate` against the direct engine port, never the transaction-mode pooler. + +# Define a model {% #model %} + +Models map to tables in the dedicated database. Define one in an app's `models.py`: + +```python +from django.db import models + + +class Article(models.Model): + title = models.CharField(max_length=200) + body = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] +``` + +Run `makemigrations` and `migrate` again to create the table, then query it through the ORM: + +```python +from blog.models import Article + +Article.objects.create(title="Hello", body="First post") + +recent = Article.objects.order_by("-created_at")[:10] +``` + +# Persistent connections and pooling {% #pooling %} + +By default Django opens a new connection per request (`CONN_MAX_AGE = 0`). On a long-running WSGI server (Gunicorn, uWSGI) you can reuse connections by raising it. Each worker thread keeps its own connection, so the database must allow at least as many connections as you run worker threads: + +```python +DATABASES["default"]["CONN_MAX_AGE"] = 60 +DATABASES["default"]["CONN_HEALTH_CHECKS"] = True +``` + +`CONN_HEALTH_CHECKS` revalidates a reused connection once per request, avoiding errors after an engine restart. Don't enable persistent connections under the development server, it spawns a thread per request and gains nothing, and disable them under ASGI. + +{% info title="Persistent connections need a session" %} +A worker holding a connection across requests behaves like a long-lived session. Connect it to the **direct** engine endpoint or the **session-mode** pooler, never the default **transaction-mode** pooler, which can hand each statement a different backend connection. The transaction-mode pooler is for serverless and short-lived runtimes that open and close a connection per invocation. +{% /info %} + +If you front the database with the transaction-mode [connection pooler](/docs/products/databases/dedicated/pooler) anyway, point `HOST`/`PORT` at the pooler (`6432` for PostgreSQL, `6033` for MySQL/MariaDB). On PostgreSQL, also set `DISABLE_SERVER_SIDE_CURSORS = True`, since server-side cursors can't survive being moved between backend connections: + +```python +DATABASES["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True +``` + +For prepared statements, advisory locks, `LISTEN`/`NOTIFY`, or temporary tables, use **session mode** instead, see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection details. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its connection details. +2. Export them as the `DB_*` environment variables your settings read. +3. Run `python manage.py migrate` and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, its schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/drivers/+page.markdoc b/src/routes/docs/products/databases/dedicated/drivers/+page.markdoc new file mode 100644 index 00000000000..687d9216f92 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/drivers/+page.markdoc @@ -0,0 +1,225 @@ +--- +layout: article +title: Node.js drivers +description: Connect to an Appwrite dedicated database from Node.js with native drivers — node-postgres, postgres.js, and mysql2. Covers production pool configuration, TLS and CA verification, serverless connection management, and troubleshooting. +--- + +A dedicated database is a standard engine, so any native Node.js driver talks to it over TLS with no Appwrite-specific code. The [Connect](/docs/products/databases/dedicated/connect#drivers) page shows the minimal snippet to run your first query. This page picks up where that leaves off: how to configure a production connection pool, verify the server certificate, keep serverless runtimes from exhausting connections, and read the errors when something is wrong. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Connect](/docs/products/databases/dedicated/connect#credentials) to retrieve the connection string. The default database name and username are both `appwrite`. Keep the password in an environment variable, never commit it. +{% /info %} + +# Raw driver, ORM, or SQL API? {% #choosing %} + +There are three ways to reach a dedicated database from Node.js. Pick by workload, not habit: + +| Approach | Use it when | +|------------------------------|----------------------------------------------------------------------------------------------| +| **Raw driver** (this page) | You want a connection pool you control, hot-path queries, or a thin data layer with no ORM overhead. | +| **ORM** ([Prisma](/docs/products/databases/dedicated/prisma)) | You want migrations, a typed schema, and query building. The ORM sits on top of these same drivers. | +| **[SQL API](/docs/products/databases/dedicated/sql-api)** | You're on an edge runtime that can't hold a TCP socket (Cloudflare Workers, Vercel Edge), or scripting a one-off query over HTTPS. | + +The rest of this page is about the raw-driver path on a long-running or serverless Node.js server that *can* open TCP connections. + +# node-postgres (pg) {% #node-postgres %} + +For a long-running server, create one `Pool` at startup and reuse it for every request. The pool opens connections lazily up to `max` and hands them back to your handlers. Never create a `Pool` per request. + +```js +import { Pool } from 'pg'; +import process from 'node:process'; + +const pool = new Pool({ + host: 'db--..appwrite.center', + port: 5432, + user: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: { rejectUnauthorized: true }, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, +}); + +const { rows } = await pool.query('SELECT id, email FROM users WHERE id = $1', [userId]); +``` + +Pool sizing is a budget, not a target. Each connection in `max`, across every running instance, counts against the database's [`networkMaxConnections`](/docs/products/databases/dedicated/network#connections) ceiling. A single app server with `max: 10` is fine; ten replicas with `max: 50` each is 500 connections and will exhaust most specifications. Size `max` to `ceiling ÷ replica count`, then put the [pooler](/docs/products/databases/dedicated/pooler) in front if you need more client concurrency than that allows. + +`ssl: { rejectUnauthorized: true }` validates the server certificate against Node's built-in CA store. The edge proxy presents a certificate signed by a well-known public CA, so this works without supplying your own bundle. See [SSL and CA verification](#tls) below for `verify-full` and mTLS. + +## Pooled port vs direct port {% #pg-ports %} + +The pooler defaults to **transaction mode**, which does not hold a backend connection across statements, so server-side prepared statements are unavailable. In node-postgres a query becomes a server-side prepared statement only when you pass a `name` field on the query config: + +```js +// Safe on the pooler (port 6432, transaction mode): no `name`, no server-side prepared statement. +await pool.query('SELECT * FROM events WHERE user_id = $1', [userId]); + +// NOT safe on the pooler in transaction mode: `name` creates a per-connection prepared statement. +await pool.query({ name: 'fetch-events', text: 'SELECT * FROM events WHERE user_id = $1', values: [userId] }); +``` + +Parameterised queries without a `name` are sent fresh each time and work on the pooled port. If you rely on named prepared statements, advisory locks, `LISTEN`/`NOTIFY`, or `SET LOCAL`, connect on the **engine port** (`5432`) or switch the pooler to **session mode** — see the [pooler modes](/docs/products/databases/dedicated/pooler#modes) page. Run migrations on the engine port regardless. + +# postgres.js {% #postgres-js %} + +`postgres.js` (the `postgres` package) takes its own option names. Create the client once at module scope: + +```js +import postgres from 'postgres'; +import process from 'node:process'; + +const sql = postgres({ + host: 'db--..appwrite.center', + port: 5432, + username: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: 'require', + max: 10, + idle_timeout: 30, + connect_timeout: 10, +}); + +const users = await sql`SELECT id, email FROM users WHERE id = ${userId}`; +``` + +`ssl: 'require'` enables TLS and validates against the system CA store. If your runtime image ships without one, pass an object with a public CA bundle instead: `ssl: { ca: fs.readFileSync('./ca-bundle.crt') }` (see [below](#tls)). + +When you point `postgres.js` at the **pooler port** (`6432`) in transaction mode, disable prepared statements — `postgres.js` uses them by default and the library documents `prepare: false` specifically for PgBouncer transaction mode: + +```js +const sql = postgres({ + host: 'db--..appwrite.center', + port: 6432, + username: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: 'require', + prepare: false, + max: 5, +}); +``` + +Leave `prepare` at its default when you connect on the engine port or use the pooler in session mode. + +# mysql2 (MySQL / MariaDB) {% #mysql2 %} + +Use `mysql2/promise` and `createPool` for MySQL and MariaDB. The same pool serves both engines: + +```js +import mysql from 'mysql2/promise'; +import process from 'node:process'; + +const pool = mysql.createPool({ + host: 'db--..appwrite.center', + port: 3306, + user: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: { rejectUnauthorized: true }, + connectionLimit: 10, + waitForConnections: true, + queueLimit: 0, +}); + +const [rows] = await pool.query('SELECT id, email FROM users WHERE id = ?', [userId]); +``` + +`connectionLimit` is the mysql2 equivalent of pg's `max` — budget it against `networkMaxConnections` the same way. `waitForConnections: true` queues callers when the pool is saturated rather than throwing; `queueLimit: 0` leaves that queue unbounded (set a number to apply backpressure). + +MySQL 8 authenticates with the `caching_sha2_password` plugin, which **requires** a TLS connection. The `ssl` block above satisfies that. The pooler ports for MySQL and MariaDB are `6033`; route serverless traffic there and use `3306` for migrations. + +# SSL and CA verification {% #tls %} + +The edge proxy terminates TLS for every dedicated database, and the connection string Appwrite returns already requests it (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB). Two levels of strictness: + +| Level | node-postgres / mysql2 | postgres.js | +|----------------------------------------------|----------------------------------------------------------|--------------------------------------------------| +| **Encrypt + verify CA** | `ssl: { rejectUnauthorized: true }` | `ssl: 'require'` | +| **Custom CA bundle** (no system trust store) | `ssl: { rejectUnauthorized: true, ca: fs.readFileSync('./ca-bundle.crt') }` | `ssl: { ca: fs.readFileSync('./ca-bundle.crt') }` | + +`rejectUnauthorized: true` (the default for these drivers when `ssl` is an object) verifies the proxy's certificate, chain and hostname, against the public CA bundle Node already trusts — that is full verification with no extra files. The certificate behind every database hostname is signed by a well-known public CA; there is no Appwrite-specific CA to download. The `ca` option exists for runtimes without a system trust store (distroless or scratch images): point it at a standard public CA bundle, from your base image's `ca-certificates` package or [Mozilla's bundle](https://curl.se/docs/caextract.html): + +```js +import fs from 'node:fs'; + +const ssl = { + rejectUnauthorized: true, + ca: fs.readFileSync('./ca-bundle.crt'), +}; +``` + +When the CA comes from an environment variable rather than a file, restore the newlines the variable strips: + +```js +const ssl = { + rejectUnauthorized: true, + ca: process.env.DB_SSL_CA?.replace(/\\n/g, '\n'), +}; +``` + +Never ship `rejectUnauthorized: false` to production — it disables certificate validation and exposes the connection to interception. To require **mTLS** (the client also presents a certificate), see the [Network](/docs/products/databases/dedicated/network) page; you then add `key` and `cert` alongside `ca` in the same `ssl` object. + +# Serverless connection management {% #serverless %} + +On Lambda, Cloud Run, Vercel, and similar platforms, every cold start is a fresh instance with its own pool. A `max: 10` pool times 200 concurrent instances is 2,000 backend connections — far past any specification. The fix is to stop trying to pool inside the instance and let Appwrite's pooler absorb the fan-out: + +- **Connect on the pooler port** (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) in transaction mode. +- **Keep the per-instance pool tiny** — `max: 1` or `2`. One invocation rarely needs more than one connection at a time. +- **Create the client once per module scope** so warm invocations reuse it instead of reconnecting on every request. +- **Disable per-connection prepared statements** on the pooled port: `prepare: false` for postgres.js, and avoid `name` on node-postgres query configs. + +```js +import postgres from 'postgres'; +import process from 'node:process'; + +// Module scope: reused across warm invocations. +const sql = postgres({ + host: 'db--..appwrite.center', + port: 6432, + username: 'appwrite', + password: process.env.DB_PASSWORD, + database: 'appwrite', + ssl: 'require', + prepare: false, + max: 1, +}); + +export async function handler(event) { + const rows = await sql`SELECT id FROM users WHERE email = ${event.email}`; + return rows[0] ?? null; +} +``` + +On **edge runtimes** (Cloudflare Workers, Vercel Edge, Deno Deploy) you usually can't open a raw TCP socket at all. Use the [SQL API](/docs/products/databases/dedicated/sql-api) instead — it runs one parameterised statement over HTTPS and returns JSON, with no driver and no connection to manage. + +# Troubleshooting {% #troubleshooting %} + +| Symptom | Cause and fix | +|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| `self-signed certificate` / `unable to verify the first certificate` | TLS is reaching the wrong endpoint, or you pinned a stale or incomplete `ca` bundle. The proxy's certificate is signed by a public CA: use `rejectUnauthorized: true` with no `ca`, and supply a current public CA bundle only when the runtime has no system trust store. Do **not** set `rejectUnauthorized: false`. | +| `sorry, too many clients already` / connection attempts rejected at the proxy | Aggregate pool size across all instances exceeds [`networkMaxConnections`](/docs/products/databases/dedicated/network#connections). Lower `max`/`connectionLimit`, or move to the [pooler](/docs/products/databases/dedicated/pooler) port and shrink the per-instance pool to 1–2. | +| `prepared statement "S_1" does not exist` / `unnamed prepared statement does not exist` | Server-side prepared statements on the pooler in transaction mode. Set `prepare: false` (postgres.js), drop the `name` field (node-postgres), or use the engine port / [session mode](/docs/products/databases/dedicated/pooler#modes). | +| MySQL `ER_NOT_SUPPORTED_AUTH_MODE` / auth-plugin error | `caching_sha2_password` needs TLS. Add the `ssl` block (or use the connection string, which already sets `ssl=true`). Don't downgrade the auth plugin. | +| `ETIMEDOUT` / `connection timed out` on connect | The database may be cold-starting (Free spec) or blocked by an [IP allowlist](/docs/products/databases/dedicated/network#allowlist). Confirm your egress IP is allowed and raise `connectionTimeoutMillis` / `connect_timeout` to absorb the cold start. | +| Connections drop after a period of inactivity | The proxy closes idle connections after `networkIdleTimeoutSeconds`. Keep `idleTimeoutMillis`/`idle_timeout` below that window so the driver recycles before the proxy does, or enable keep-alive. | + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/sql-api" title="SQL API" %} +Run SQL over HTTPS with no TCP connection — for edge runtimes and scripts. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/drizzle/+page.markdoc b/src/routes/docs/products/databases/dedicated/drizzle/+page.markdoc new file mode 100644 index 00000000000..42ccc2f51a9 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/drizzle/+page.markdoc @@ -0,0 +1,383 @@ +--- +layout: article +title: Drizzle +description: Use Drizzle ORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure the driver, run migrations against the direct endpoint, and pool through the connection pooler from serverless runtimes. +--- + +A dedicated database is a standard engine, so [Drizzle ORM](https://orm.drizzle.team/) works against it with no Appwrite-specific configuration. You point Drizzle's driver at the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and use Drizzle Kit, the query builder, and the rest of the toolchain exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Set the connection string {% #connection-string %} + +Copy the connection string from the Console **Connect** view, or fetch it from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials). Put it in your environment, never commit it: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MariaDB uses the same `mysql://` scheme and ports as MySQL. +{% /tabsitem %} +{% /tabs %} + +The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns. The edge proxy terminates TLS for every dedicated database, so no extra certificate configuration is needed. For full certificate verification (`verify-full`) or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Install and configure the driver {% #driver %} + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +Drizzle talks to PostgreSQL through one of two driver packages. Pick the one already in your stack: + +```bash +# node-postgres +npm i drizzle-orm pg +npm i -D drizzle-kit @types/pg + +# or postgres.js +npm i drizzle-orm postgres +npm i -D drizzle-kit +``` + +With **node-postgres**, import `drizzle` from `drizzle-orm/node-postgres` and hand it the connection string: + +```ts +import { drizzle } from 'drizzle-orm/node-postgres'; + +export const db = drizzle(process.env.DATABASE_URL!); +``` + +With **postgres.js**, import `drizzle` from `drizzle-orm/postgres-js`. For a direct connection to the engine you can pass the URL straight through: + +```ts +import { drizzle } from 'drizzle-orm/postgres-js'; + +export const db = drizzle(process.env.DATABASE_URL!); +``` + +When you pool through the connection pooler in transaction mode (see [Pool connections from serverless](#pooling)), postgres.js must disable prepared statements. Build the client yourself with `prepare: false` and pass it to `drizzle`: + +```ts +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; + +const client = postgres(process.env.DATABASE_URL!, { prepare: false }); + +export const db = drizzle({ client }); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +Install `mysql2` and import `drizzle` from `drizzle-orm/mysql2`: + +```bash +npm i drizzle-orm mysql2 +npm i -D drizzle-kit +``` + +```ts +import { drizzle } from 'drizzle-orm/mysql2'; + +export const db = drizzle(process.env.DATABASE_URL!); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +The `mysql2` driver serves both MySQL and MariaDB. Install it and import `drizzle` from `drizzle-orm/mysql2`: + +```bash +npm i drizzle-orm mysql2 +npm i -D drizzle-kit +``` + +```ts +import { drizzle } from 'drizzle-orm/mysql2'; + +export const db = drizzle(process.env.DATABASE_URL!); +``` +{% /tabsitem %} +{% /tabs %} + +Define your tables in a schema file Drizzle Kit can read: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: serial('id').primaryKey(), + email: text('email').notNull().unique(), + createdAt: timestamp('created_at').notNull().defaultNow() +}); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +import { mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core'; + +export const users = mysqlTable('users', { + id: serial('id').primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + createdAt: timestamp('created_at').notNull().defaultNow() +}); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ts +import { mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core'; + +export const users = mysqlTable('users', { + id: serial('id').primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + createdAt: timestamp('created_at').notNull().defaultNow() +}); +``` +{% /tabsitem %} +{% /tabs %} + +# Configure Drizzle Kit {% #config %} + +Drizzle Kit reads `drizzle.config.ts` for migrations and introspection. Set the `dialect`, point `schema` at your table definitions, and pass the connection string through `dbCredentials`: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'postgresql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_URL! + } +}); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'mysql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_URL! + } +}); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'mysql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_URL! + } +}); +``` + +The `mysql` dialect drives MariaDB too. +{% /tabsitem %} +{% /tabs %} + +# Run migrations {% #migrate %} + +Generate SQL migration files from your schema, then apply them: + +```bash +npx drizzle-kit generate +npx drizzle-kit migrate +``` + +`generate` diffs your schema against the last snapshot and writes a timestamped `.sql` file into the `out` directory; `migrate` applies any pending files to the database. Point Drizzle Kit at the **direct** engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB), not the pooler, migrations issue DDL that needs a real session-level connection. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +To apply migrations from your application at startup instead of the CLI, use the matching `migrate` helper for your driver: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { db } from './db'; + +await migrate(db, { migrationsFolder: './drizzle' }); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +import { migrate } from 'drizzle-orm/mysql2/migrator'; +import { db } from './db'; + +await migrate(db, { migrationsFolder: './drizzle' }); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ts +import { migrate } from 'drizzle-orm/mysql2/migrator'; +import { db } from './db'; + +await migrate(db, { migrationsFolder: './drizzle' }); +``` +{% /tabsitem %} +{% /tabs %} + +# Query with Drizzle {% #query %} + +Once the schema is migrated, use the query builder: + +```ts +import { desc } from 'drizzle-orm'; +import { db } from './db'; +import { users } from './schema'; + +const [user] = await db + .insert(users) + .values({ email: 'ada@example.com' }) + .returning(); + +const recent = await db + .select() + .from(users) + .orderBy(desc(users.createdAt)) + .limit(10); +``` + +On long-running servers, create the `db` instance once at module scope and reuse it. On serverless, keep a single instance per module scope so warm invocations reuse it, and rely on the pooler to absorb cold-start connection churn. + +# Pool connections from serverless {% #pooling %} + +Each running instance opens its own connections to the engine. On serverless and edge platforms (Vercel, Netlify, Cloudflare) where every invocation is a fresh instance, that fans out into far more backend connections than the engine allows. Route runtime traffic through the [connection pooler](/docs/products/databases/dedicated/pooler) instead by connecting on the pooler port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) on the same hostname. + +The pooler defaults to **transaction mode**, which does not keep a backend connection across statements, so server-side prepared statements are unavailable. Keep `drizzle.config.ts` and the startup migrator pointed at `DIRECT_URL` so DDL still runs over a real session connection: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` + +With **postgres.js**, disable prepared statements by building the client with `prepare: false`, as shown in [Install and configure the driver](#driver). + +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'postgresql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DIRECT_URL! + } +}); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'mysql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DIRECT_URL! + } +}); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +```ts +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'mysql', + schema: './src/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DIRECT_URL! + } +}); +``` +{% /tabsitem %} +{% /tabs %} + +If your application relies on prepared statements, advisory locks, `LISTEN`/`NOTIFY`, temporary tables, or `SET LOCAL`, switch the pooler to **session mode** (and drop `prepare: false` with postgres.js), see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Export it as `DIRECT_URL` (and the pooled variant as `DATABASE_URL`). +3. Run `drizzle-kit migrate` and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/ef-core/+page.markdoc b/src/routes/docs/products/databases/dedicated/ef-core/+page.markdoc new file mode 100644 index 00000000000..b83ba0723b1 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/ef-core/+page.markdoc @@ -0,0 +1,296 @@ +--- +layout: article +title: EF Core +description: Use Entity Framework Core with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure the provider connection string, run migrations against the direct endpoint, and rely on the driver's built-in connection pool from an ASP.NET server. +--- + +A dedicated database is a standard engine, so [Entity Framework Core](https://learn.microsoft.com/ef/core/) works against it with no Appwrite-specific configuration. You point the engine's EF Core provider at the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and use `DbContext`, migrations, and the rest of the toolchain exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection details. The default database name and username are both `appwrite`. +{% /info %} + +# Install the provider {% #install %} + +Add the EF Core provider for your engine: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```bash +dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```bash +dotnet add package Pomelo.EntityFrameworkCore.MySql +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```bash +dotnet add package Pomelo.EntityFrameworkCore.MySql +``` + +The Pomelo provider drives both MySQL and MariaDB, `ServerVersion.AutoDetect(...)` detects either server, so no MariaDB-specific package is needed. +{% /tabsitem %} +{% /tabs %} + +Then add the EF Core design-time package (used by the `dotnet ef` tools) and install the CLI tool: + +```bash +dotnet add package Microsoft.EntityFrameworkCore.Design + +dotnet tool install --global dotnet-ef +``` + +# Set the connection string {% #connection-string %} + +ADO.NET providers use key/value connection strings rather than a URL. Store it in `appsettings.json` under `ConnectionStrings`, and keep the password out of source control (use [user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets) or an environment variable in real deployments): + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```json +{ + "ConnectionStrings": { + "Default": "Host=db--..appwrite.center;Port=5432;Database=appwrite;Username=appwrite;Password=;SSL Mode=Require" + } +} +``` + +The edge proxy terminates TLS for every dedicated database, so `SSL Mode=Require` is all you need. From Npgsql 6.0 onward, `Require` encrypts the connection without validating the server certificate, the equivalent of also setting `Trust Server Certificate=true`. For full certificate verification, set `SSL Mode=VerifyFull`: Npgsql validates the chain and hostname against the operating system's trusted roots, and the proxy's certificate is signed by a public CA those roots already include, so no `Root Certificate` parameter is needed: + +```text +Host=...;Port=5432;Database=appwrite;Username=appwrite;Password=;SSL Mode=VerifyFull +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```json +{ + "ConnectionStrings": { + "Default": "Server=db--..appwrite.center;Port=3306;Database=appwrite;User=appwrite;Password=;SslMode=Required" + } +} +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS, so keep `SslMode=Required`. `Required` encrypts the connection without validating the server certificate. For full certificate verification, set `SslMode=VerifyFull`: MySqlConnector validates the chain and hostname against the operating system's trusted roots, and the proxy's certificate is signed by a public CA those roots already include, so no `SslCa` parameter is needed: + +```text +Server=...;Port=3306;Database=appwrite;User=appwrite;Password=;SslMode=VerifyFull +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```json +{ + "ConnectionStrings": { + "Default": "Server=db--..appwrite.center;Port=3306;Database=appwrite;User=appwrite;Password=;SslMode=Required" + } +} +``` + +MariaDB takes the same MySqlConnector connection string as MySQL, and `SslMode=VerifyFull` enables full certificate verification the same way. +{% /tabsitem %} +{% /tabs %} + +# Configure the DbContext {% #dbcontext %} + +Define your model and a `DbContext`. Both are engine-agnostic, the provider-specific call comes when you register the context in `Program.cs`: + +```csharp +using Microsoft.EntityFrameworkCore; + +public class User +{ + public int Id { get; set; } + public string Email { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) + : base(options) { } + + public DbSet Users => Set(); +} +``` + +# Run migrations {% #migrate %} + +Migrations need a real session connection and full DDL privileges, so always run them against the **direct** engine endpoint (port `5432` for PostgreSQL, `3306` for MySQL/MariaDB), never the [pooler](/docs/products/databases/dedicated/pooler). The primary `appwrite` user owns the default database; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +Create the first migration, then apply it: + +```bash +dotnet ef migrations add InitialCreate + +dotnet ef database update +``` + +`dotnet ef database update` reads the same `Default` connection string and applies any pending migrations, recording each in the `__EFMigrationsHistory` table so it only applies new ones next time. In CI or production, prefer generating an idempotent SQL script with `dotnet ef migrations script --idempotent` and applying it as a deploy step. + +# Wire up an ASP.NET server {% #aspnet %} + +Register the `DbContext` in `Program.cs`, reading the connection string with `GetConnectionString`. A long-running ASP.NET server should connect to the **direct** endpoint and let the provider manage its own pool: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +Call `UseNpgsql(...)` with the connection string, this is the single entry point for all Npgsql options: + +```csharp +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} + +Call `UseMySql(...)` with the connection string and a server version, `ServerVersion.AutoDetect(...)` opens one connection to read it: + +```csharp +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +var connectionString = builder.Configuration.GetConnectionString("Default"); + +builder.Services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} + +The call is identical to MySQL, `ServerVersion.AutoDetect(...)` recognises a MariaDB server and configures the provider accordingly: + +```csharp +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +var connectionString = builder.Configuration.GetConnectionString("Default"); + +builder.Services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); +``` +{% /tabsitem %} +{% /tabs %} + +The endpoints are engine-agnostic: + +```csharp +var app = builder.Build(); + +app.MapGet("/users", async (AppDbContext db) => + await db.Users.OrderByDescending(u => u.CreatedAt).Take(10).ToListAsync()); + +app.MapPost("/users", async (AppDbContext db, string email) => +{ + var user = new User { Email = email }; + db.Users.Add(user); + await db.SaveChangesAsync(); + return Results.Created($"/users/{user.Id}", user); +}); + +app.Run(); +``` + +# Connection pooling {% #pooling %} + +Npgsql and MySqlConnector (the ADO.NET driver behind Pomelo) both have a **built-in connection pool**, enabled by default. Each process keeps its own pool keyed on the connection string, so a long-running ASP.NET server doesn't need the Appwrite connection pooler, point it at the **direct** engine endpoint and tune the pool with the connection string instead: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```text +Host=...;Port=5432;Database=appwrite;Username=appwrite;Password=;SSL Mode=Require;Maximum Pool Size=50 +``` + +Set `Maximum Pool Size` to cap concurrent backend connections per process (Npgsql defaults to `100`). +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```text +Server=...;Port=3306;Database=appwrite;User=appwrite;Password=;SslMode=Required;MaximumPoolSize=50 +``` + +Set `MaximumPoolSize` to cap concurrent backend connections per process (MySqlConnector defaults to `100`). +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```text +Server=...;Port=3306;Database=appwrite;User=appwrite;Password=;SslMode=Required;MaximumPoolSize=50 +``` + +Set `MaximumPoolSize` to cap concurrent backend connections per process (MySqlConnector defaults to `100`). +{% /tabsitem %} +{% /tabs %} + +Keep the sum across all your processes under the engine's connection limit for the database's specification. + +If you instead run many short-lived instances (serverless functions, per-request containers) that each open their own pool, route them through the Appwrite [connection pooler](/docs/products/databases/dedicated/pooler) (port `6432` for PostgreSQL, `6033` for MySQL/MariaDB) to fan many clients onto a small backend pool. The pooler defaults to **transaction mode**, which doesn't hold a backend connection across statements, so server-side prepared statements aren't available: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +Disable Npgsql's automatic prepared statements with `Max Auto Prepare=0` when connecting to the transaction-mode pooler: + +```text +Host=...;Port=6432;Database=appwrite;Username=appwrite;Password=;SSL Mode=Require;Max Auto Prepare=0 +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} + +MySqlConnector only prepares statements when you call `Prepare()` explicitly, so switching to the pooler port is all that's needed: + +```text +Server=...;Port=6033;Database=appwrite;User=appwrite;Password=;SslMode=Required +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} + +MySqlConnector only prepares statements when you call `Prepare()` explicitly, so switching to the pooler port is all that's needed: + +```text +Server=...;Port=6033;Database=appwrite;User=appwrite;Password=;SslMode=Required +``` +{% /tabsitem %} +{% /tabs %} + +If your application relies on prepared statements, advisory locks, or `LISTEN`/`NOTIFY`, switch the pooler to **session mode** instead, see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and credentials. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its connection details. +2. Build the connection string for the branch endpoint and pass it as the `Default` connection string. +3. Run `dotnet ef database update` and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/fastapi/+page.markdoc b/src/routes/docs/products/databases/dedicated/fastapi/+page.markdoc new file mode 100644 index 00000000000..d87943ab490 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/fastapi/+page.markdoc @@ -0,0 +1,186 @@ +--- +layout: article +title: FastAPI +description: Use FastAPI and SQLAlchemy 2.x with an Appwrite dedicated PostgreSQL database. Configure the async asyncpg engine, pass TLS through connect_args, inject a session per request, and run Alembic migrations against the direct endpoint. +--- + +A dedicated database is a standard PostgreSQL engine, so [FastAPI](https://fastapi.tiangolo.com/) with [SQLAlchemy](https://docs.sqlalchemy.org/) and an async driver works against it with no Appwrite-specific configuration. You point `create_async_engine` at the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and use the SQLAlchemy ORM, the FastAPI dependency system, and Alembic exactly as you would against any self-hosted PostgreSQL server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Install dependencies {% #install %} + +```bash +pip install "fastapi[standard]" "sqlalchemy[asyncio]" asyncpg alembic +``` + +# Set the connection string {% #connection-string %} + +Copy the connection string from the Console **Connect** view, or fetch it from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials). Put it in your environment, never commit it. SQLAlchemy's asyncpg dialect uses the `postgresql+asyncpg://` scheme, so swap the leading `postgres://` for it: + +```env +DATABASE_URL="postgresql+asyncpg://appwrite:@db--..appwrite.center:5432/appwrite" +``` + +The string Appwrite returns ends with `?sslmode=require`. Drop that query parameter for asyncpg, asyncpg does not read `sslmode` from the URL. TLS is configured through `connect_args` instead, shown below. + +This guide uses the [asyncpg](https://magicstack.github.io/asyncpg/current/) driver; the same patterns apply to the async `postgresql+psycopg` dialect by changing the URL scheme. + +# Create the async engine {% #engine %} + +The edge proxy terminates TLS for every dedicated database, so encryption is mandatory. asyncpg's `ssl` argument accepts the libpq-style strings (`require`, `verify-ca`, `verify-full`), `True`, or an `ssl.SSLContext`. Pass it through `connect_args`: + +```python +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +engine = create_async_engine( + settings.database_url, + connect_args={"ssl": "require"}, + pool_size=10, + max_overflow=5, + pool_pre_ping=True, +) + +SessionLocal = async_sessionmaker(engine, expire_on_commit=False) +``` + +`ssl="require"` encrypts the connection without validating the certificate chain. For full verification, pass `ssl="verify-full"` instead; the server certificate is signed by a well-known public CA, so validation succeeds against the system trust store without a custom CA bundle, see the [Network](/docs/products/databases/dedicated/network) page. + +# Define a model {% #model %} + +```python +from datetime import datetime + +from sqlalchemy import func +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +class Base(DeclarativeBase): + pass + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(unique=True) + created_at: Mapped[datetime] = mapped_column(server_default=func.now()) +``` + +# Inject a session per request {% #session %} + +FastAPI's dependency system gives each request its own `AsyncSession` and closes it when the request finishes. Define a dependency that yields a session, then annotate path operations with it: + +```python +from typing import Annotated + +from fastapi import Depends, FastAPI +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +app = FastAPI() + +async def get_session(): + async with SessionLocal() as session: + yield session + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +@app.post("/users") +async def create_user(email: str, session: SessionDep): + user = User(email=email) + session.add(user) + await session.commit() + await session.refresh(user) + return user + +@app.get("/users") +async def list_users(session: SessionDep): + result = await session.scalars(select(User).order_by(User.created_at.desc())) + return result.all() +``` + +Create the engine once at module scope and reuse it for the whole process, the pool lives inside it. Don't open a new engine per request. + +# Run migrations {% #migrate %} + +Generate Alembic's async scaffold, which opens the connection asynchronously and hands a sync connection to the migration context: + +```bash +alembic init -t async migrations +``` + +Point `target_metadata` at `Base.metadata` in `migrations/env.py`, then autogenerate and apply: + +```bash +alembic revision --autogenerate -m "init" +alembic upgrade head +``` + +Run migrations against the **direct** engine port (`5432`), not the pooler. DDL needs a real session-level connection, and Alembic's async template already uses a `NullPool`, so a fresh connection is opened and closed per run. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +# Pool sizing {% #sizing %} + +A long-running `uvicorn` server holds a SQLAlchemy pool for its lifetime, so connect to the **direct** engine port (`5432`) with a sized pool. Keep `pool_size` × the number of server processes under the connection limit of your [specification](/docs/products/databases/dedicated/specifications), and let `max_overflow` absorb short bursts: + +```python +engine = create_async_engine( + settings.database_url, + connect_args={"ssl": "require"}, + pool_size=10, + max_overflow=5, +) +``` + +When the same app runs on a serverless platform (for example an Appwrite [function](/docs/products/functions)) where each invocation is a fresh instance, that fans out into far more backend connections than the engine allows. Route runtime traffic through the [connection pooler](/docs/products/databases/dedicated/pooler) on port `6432` instead, and size the pool small per instance: + +```env +DATABASE_URL="postgresql+asyncpg://appwrite:@db--..appwrite.center:6432/appwrite" +``` + +# Disable prepared statements on the transaction pooler {% #prepared-statements %} + +The pooler defaults to **transaction mode**, which does not keep a backend connection across statements. asyncpg relies on server-side prepared statements, which transaction mode cannot support, so you must turn the caches off. This needs **two** settings, asyncpg's own `statement_cache_size` and SQLAlchemy's dialect-level `prepared_statement_cache_size`: + +```python +from sqlalchemy import NullPool + +engine = create_async_engine( + settings.database_url, + connect_args={"ssl": "require", "statement_cache_size": 0}, + prepared_statement_cache_size=0, + poolclass=NullPool, +) +``` + +`statement_cache_size=0` (in `connect_args`) disables asyncpg's prepared statement cache, and `prepared_statement_cache_size=0` (a `create_async_engine` keyword) disables the dialect's own per-connection statement cache. Without both, asyncpg still emits prepared statements and the transaction-mode pooler rejects them. Use `NullPool` so SQLAlchemy doesn't keep its own pool on top of the pooler's. + +If your application relies on prepared statements, advisory locks, `LISTEN`/`NOTIFY`, or temporary tables, switch the pooler to **session mode** instead and keep statement caching on, see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Rewrite the scheme to `postgresql+asyncpg://` and export it as `DATABASE_URL`. +3. Run `alembic upgrade head` against the branch's direct port, then your test suite. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/gorm/+page.markdoc b/src/routes/docs/products/databases/dedicated/gorm/+page.markdoc new file mode 100644 index 00000000000..ad20d9243c1 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/gorm/+page.markdoc @@ -0,0 +1,329 @@ +--- +layout: article +title: GORM +description: Use GORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database in Go. Build the DSN, open a connection, size the database/sql pool against the direct endpoint, and run migrations with AutoMigrate or golang-migrate. +--- + +A dedicated database is a standard engine, so [GORM](https://gorm.io/) talks to it with no Appwrite-specific configuration. You build a connection string from the credentials on the [Connect](/docs/products/databases/dedicated/connect) page, hand it to the matching GORM driver, and use models, `AutoMigrate`, and the query API exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the host, password, and connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Build the DSN {% #dsn %} + +The edge proxy terminates TLS for every dedicated database, so no certificate file is needed. Keep the password out of source and read it from the environment: + +```env +DB_HOST="db--..appwrite.center" +DB_PASSWORD="" +``` + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +GORM's PostgreSQL driver accepts the pgx key/value DSN. Set `sslmode=require`: + +```go +dsn := fmt.Sprintf( + "host=%s user=appwrite password=%s dbname=appwrite port=5432 sslmode=require", + os.Getenv("DB_HOST"), os.Getenv("DB_PASSWORD"), +) +``` + +The URL form the credentials endpoint returns works too, the driver parses either: `postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require`. For full certificate verification, add `sslmode=verify-full`, no CA file needed: Go's drivers validate against the system certificate pool, and the proxy's certificate is signed by a public CA. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} + +GORM's MySQL driver uses the go-sql-driver DSN, which is not a URL: the `ssl=true` in the connection string Appwrite returns becomes `tls=true` here, and `parseTime=true` makes `DATETIME` columns scan into `time.Time`: + +```go +dsn := fmt.Sprintf( + "appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), +) +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS, so keep `tls=true` set. The flag also verifies the server certificate against the system certificate pool, no CA file needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} + +MariaDB uses the same `gorm.io/driver/mysql` driver (go-sql-driver/mysql underneath) and the same DSN: + +```go +dsn := fmt.Sprintf( + "appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), +) +``` +{% /tabsitem %} +{% /tabs %} + +For mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Open a connection {% #open %} + +Pass the DSN to the driver's `Open` function and call `gorm.Open`: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```go +package main + +import ( + "fmt" + "os" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func main() { + dsn := fmt.Sprintf( + "host=%s user=appwrite password=%s dbname=appwrite port=5432 sslmode=require", + os.Getenv("DB_HOST"), os.Getenv("DB_PASSWORD"), + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + panic(err) + } + + _ = db +} +``` + +Connect to the **direct** engine endpoint (port `5432`) for a long-running server, see [pool sizing](#pool) below. If you route through the [connection pooler](/docs/products/databases/dedicated/pooler) in its default transaction mode, use `postgres.New` with `PreferSimpleProtocol: true` so the driver stops issuing implicit prepared statements, which transaction mode cannot keep across statements: + +```go +db, err := gorm.Open(postgres.New(postgres.Config{ + DSN: dsn, // pooler host, port 6432 + PreferSimpleProtocol: true, +}), &gorm.Config{}) +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```go +package main + +import ( + "fmt" + "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func main() { + dsn := fmt.Sprintf( + "appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), + ) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic(err) + } + + _ = db +} +``` + +Connect to the **direct** engine endpoint (port `3306`) for a long-running server, see [pool sizing](#pool) below. To route through the [connection pooler](/docs/products/databases/dedicated/pooler), only the port changes: use `6033` on the same hostname. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```go +package main + +import ( + "fmt" + "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func main() { + dsn := fmt.Sprintf( + "appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), + ) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic(err) + } + + _ = db +} +``` + +Connect to the **direct** engine endpoint (port `3306`) for a long-running server, see [pool sizing](#pool) below. To route through the [connection pooler](/docs/products/databases/dedicated/pooler), only the port changes: use `6033` on the same hostname. +{% /tabsitem %} +{% /tabs %} + +# Define a model and migrate {% #model %} + +Declare your models as Go structs and let GORM create the tables with `AutoMigrate`: + +```go +type User struct { + ID uint `gorm:"primaryKey"` + Email string `gorm:"uniqueIndex"` + CreatedAt time.Time +} + +if err := db.AutoMigrate(&User{}); err != nil { + panic(err) +} +``` + +`AutoMigrate` creates the table if it's missing and adds any missing columns and indexes. It does not drop columns or change existing column types, so it's convenient in development but not a substitute for versioned migrations in production. Either way, run schema changes against the **direct** endpoint (the engine port): DDL needs a real session connection, and the primary `appwrite` user owns the default database. Scoped [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +For versioned migrations, [golang-migrate](https://github.com/golang-migrate/migrate) runs ordered up/down files. Point it at the direct endpoint: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```bash +migrate -path ./migrations \ + -database "postgres://appwrite:$DB_PASSWORD@$DB_HOST:5432/appwrite?sslmode=require" \ + up +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```bash +migrate -path ./migrations \ + -database "mysql://appwrite:$DB_PASSWORD@tcp($DB_HOST:3306)/appwrite?tls=true" \ + up +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```bash +migrate -path ./migrations \ + -database "mysql://appwrite:$DB_PASSWORD@tcp($DB_HOST:3306)/appwrite?tls=true" \ + up +``` +{% /tabsitem %} +{% /tabs %} + +# Size the connection pool {% #pool %} + +GORM manages a `database/sql` pool under the hood. A long-running Go server holds that pool for its whole lifetime, so connect to the **direct** endpoint and cap the pool yourself rather than letting it grow unbounded against the engine's connection limit. Reach the underlying `*sql.DB` with `db.DB()`: + +```go +sqlDB, err := db.DB() +if err != nil { + panic(err) +} + +sqlDB.SetMaxOpenConns(25) +sqlDB.SetMaxIdleConns(25) +sqlDB.SetConnMaxLifetime(time.Hour) +``` + +Keep the sum of `SetMaxOpenConns` across every instance below the specification's `maxConnections`. If you run many instances or a serverless/edge runtime that opens a fresh pool per invocation, route through the [connection pooler](/docs/products/databases/dedicated/pooler) instead and keep each instance's pool small, the pooler multiplexes them onto a handful of backend connections. + +# Use sqlx instead {% #sqlx %} + +If you prefer raw SQL with light struct scanning, [sqlx](https://github.com/jmoiron/sqlx) wraps `database/sql` and uses the same DSN. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +Register the `pgx` stdlib driver (or `lib/pq`) and connect: + +```go +import ( + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/jmoiron/sqlx" +) + +db, err := sqlx.Connect("pgx", + fmt.Sprintf("host=%s user=appwrite password=%s dbname=appwrite port=5432 sslmode=require", + os.Getenv("DB_HOST"), os.Getenv("DB_PASSWORD"))) +if err != nil { + panic(err) +} + +db.SetMaxOpenConns(25) +``` + +The same pooler caveat applies: in transaction mode, disable implicit prepared statements (the pgx stdlib driver exposes a simple-protocol option, and `lib/pq` can be told to skip prepares), or use [session mode](/docs/products/databases/dedicated/pooler#modes). `sslmode=require` needs no CA file, and neither does `verify-full`: Go's drivers fall back to the system certificate pool when `sslrootcert` is unset, and the proxy's certificate is signed by a public CA that pool already trusts. Set `sslrootcert` only when the container image ships without CA certificates. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} + +Register the go-sql-driver and connect: + +```go +import ( + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +db, err := sqlx.Connect("mysql", + fmt.Sprintf("appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"))) +if err != nil { + panic(err) +} + +db.SetMaxOpenConns(25) +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} + +Register the go-sql-driver and connect: + +```go +import ( + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +db, err := sqlx.Connect("mysql", + fmt.Sprintf("appwrite:%s@tcp(%s:3306)/appwrite?tls=true&parseTime=true", + os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"))) +if err != nil { + panic(err) +} + +db.SetMaxOpenConns(25) +``` +{% /tabsitem %} +{% /tabs %} + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string, ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Export the host and password into the environment your tests read. +3. Run `migrate ... up` (or `AutoMigrate`) and your test suite against the branch's direct endpoint. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/grafana/+page.markdoc b/src/routes/docs/products/databases/dedicated/grafana/+page.markdoc new file mode 100644 index 00000000000..e4c23a42bc8 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/grafana/+page.markdoc @@ -0,0 +1,115 @@ +--- +layout: article +title: Grafana +description: Connect Grafana to an Appwrite dedicated PostgreSQL database as a data source and build dashboards. Create a read-only connection user, configure the PostgreSQL data source with TLS, and provision it from YAML. +--- + +An Appwrite [dedicated database](/docs/products/databases/dedicated) exposes a standard managed PostgreSQL 18 or 17 engine, so [Grafana](https://grafana.com/) connects to it through the built-in **PostgreSQL data source** with no Appwrite-specific configuration. Point the data source at your database hostname, authenticate with a scoped read-only user, and query your tables to build dashboards and alerts. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and an [API key](/docs/advanced/platform/api-keys) (or the Console) to create a connection user. The default database name and primary username are both `appwrite`. See [Connect](/docs/products/databases/dedicated/connect) to retrieve credentials. +{% /info %} + +# Create a read-only user {% #read-only-user %} + +Dashboards should never connect as the primary `appwrite` user, which owns the database and can run DDL. Instead, create a dedicated connection user with the `readonly` role so Grafana can only run `SELECT` statements. Send a request to the [connections endpoint](/docs/products/databases/dedicated/connect#connections), replacing `` with your database ID: + +```bash +curl -X POST https://.appwrite.center/v1/compute/databases//connections \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "grafana_ro", + "database": "appwrite", + "role": "readonly" + }' +``` + +The response contains a generated password. Store it in a secret manager and use it for the Grafana data source below. The password is only returned once, so rotate the connection if it's lost. See [Connect](/docs/products/databases/dedicated/connect#connections) for the full connection-user lifecycle. + +# Choose an endpoint {% #endpoint %} + +Grafana holds its data source connections open for the lifetime of the process. Long-lived connections must use either the **direct** engine endpoint on port `5432` or a [pooler](/docs/products/databases/dedicated/pooler) running in **session mode**. + +Do not point Grafana at the **transaction-mode** pooler. Transaction mode hands a backend connection back to the pool after every statement, which breaks the session assumptions Grafana relies on for connection reuse and prepared statements. For a typical dashboard workload the direct endpoint is the simplest choice. See the [pooler modes](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Add the PostgreSQL data source {% #data-source %} + +In Grafana, open **Connections** > **Data sources** > **Add data source** and select **PostgreSQL**. Fill in the connection details using the values from your dedicated database: + +| Field | Value | +| --- | --- | +| **Host URL** | `db--..appwrite.center:5432` | +| **Database name** | `appwrite` | +| **Username** | `grafana_ro` | +| **Password** | the generated password from the connection user | +| **TLS/SSL Mode** | `require` | +| **Version** | match your engine, for example `18` or `17` | + +Regions are `fra`, `nyc`, `sfo`, `sgp`, `syd`, and `tor`. The edge proxy terminates TLS for every dedicated database, so **TLS/SSL Mode** `require` works with no certificate upload. For full certificate verification, set the mode to `verify-full` and leave the **TLS/SSL Root Certificate** field empty: Grafana validates against the host's system CA pool, and the proxy's certificate is signed by a public CA that pool already trusts. Provide a root certificate only if the Grafana host ships without CA certificates. + +Under **Connection limits**, keep the connection counts modest so Grafana doesn't exhaust the engine's connection budget. **Max open** caps total connections from this Grafana instance, **Max idle** caps pooled idle connections, and **Max lifetime** recycles connections after the given number of seconds. Select **Save & test** to verify connectivity. + +# Provision from YAML {% #provisioning %} + +Instead of configuring the data source by hand, you can [provision](https://grafana.com/docs/grafana/latest/administration/provisioning/) it declaratively. Drop a file into Grafana's `provisioning/datasources/` directory and read the password from an environment variable so it never lands in source control: + +```yaml +apiVersion: 1 + +datasources: + - name: Appwrite dedicated database + type: postgres + url: db--..appwrite.center:5432 + user: grafana_ro + jsonData: + database: appwrite + sslmode: require + postgresVersion: 1800 + maxOpenConns: 5 + maxIdleConns: 2 + maxIdleConnsAuto: true + connMaxLifetime: 14400 + secureJsonData: + password: ${GRAFANA_DB_PASSWORD} + editable: false +``` + +`postgresVersion` is the engine version times 100: use `1800` for PostgreSQL 18 or `1700` for PostgreSQL 17. For `verify-full`, set `sslmode: verify-full`, no `tlsCACert` entry is needed: Grafana validates against the host's system CA pool, and the proxy's certificate is signed by a public CA that pool already trusts. The `database` key lives under `jsonData` in current Grafana releases. See the [PostgreSQL data source](https://grafana.com/docs/grafana/latest/datasources/postgres/configure/) docs for every available field. + +# Build a panel {% #panel %} + +With the data source connected, create a dashboard and add a panel backed by it. Switch the query editor to code mode and write a read-only query against your tables. For example, to plot daily sign-ups from a `users` table over time: + +```text +SELECT + date_trunc('day', created_at) AS time, + count(*) AS signups +FROM users +GROUP BY 1 +ORDER BY 1 +``` + +Grafana maps the `time` column to the panel's time axis and `signups` to the value. Because the data source authenticates as the `readonly` user, any query that attempts to write fails at the database, which keeps a misconfigured panel from mutating production data. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated" title="Dedicated databases" %} +Overview of dedicated database engines, versions, and regions. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes and ports. Use the direct endpoint or session mode for Grafana. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, and IP allowlists. +{% /cards_item %} +{% /cards %} + +{% arrow_link href="/docs/products/databases/dedicated/connect" %} +Connect to a dedicated database +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/dedicated/laravel/+page.markdoc b/src/routes/docs/products/databases/dedicated/laravel/+page.markdoc new file mode 100644 index 00000000000..0c14bd5244b --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/laravel/+page.markdoc @@ -0,0 +1,212 @@ +--- +layout: article +title: Laravel +description: Use Laravel and Eloquent with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure the connection, run migrations against the direct endpoint, and pool serverless traffic through the connection pooler. +--- + +A dedicated database is a standard engine, so [Laravel](https://laravel.com/docs) works against it with no Appwrite-specific configuration. You point a connection in `config/database.php` at the credentials from the [Connect](/docs/products/databases/dedicated/connect) page and use Eloquent, the query builder, migrations, and queues exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the credentials. The default database name and username are both `appwrite`. +{% /info %} + +# Configure the connection {% #connection %} + +Laravel reads database credentials from `.env`. Copy the values from the Console **Connect** view, or fetch them from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials), and set the matching connection. Never commit `.env`: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DB_CONNECTION=pgsql +DB_HOST=db--..appwrite.center +DB_PORT=5432 +DB_DATABASE=appwrite +DB_USERNAME=appwrite +DB_PASSWORD= +DB_SSLMODE=require +``` + +The default `config/database.php` already wires these variables into the `pgsql` connection, including SSL: + +```php +'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'sslmode' => env('DB_SSLMODE', 'prefer'), +], +``` + +`sslmode` is a top-level key on the PostgreSQL connection (since Laravel 10, SSL settings live directly in the connection array, not under `options`). Setting `DB_SSLMODE=require` matches the `sslmode=require` that Appwrite uses, the edge proxy terminates TLS for every dedicated database, so no certificate files are needed for `require`. For full certificate verification, set `DB_SSLMODE=verify-full` and point `sslrootcert` at a trusted root store, `system` on libpq 16+, or your OS bundle such as `/etc/ssl/certs/ca-certificates.crt`, the proxy's certificate is signed by a public CA, so there is no Appwrite-specific CA to download: + +```php +'sslmode' => env('DB_SSLMODE', 'prefer'), +'sslrootcert' => env('DB_SSLROOTCERT'), +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DB_CONNECTION=mysql +DB_HOST=db--..appwrite.center +DB_PORT=3306 +DB_DATABASE=appwrite +DB_USERNAME=appwrite +DB_PASSWORD= +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS. The proxy terminates TLS for you, so the connection is encrypted without extra flags. To pin a CA for verification, set the `MYSQL_ATTR_SSL_CA` environment variable, which the default `mysql` connection passes to PDO through its `options` array: + +```env +MYSQL_ATTR_SSL_CA=/path/to/ca.pem +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DB_CONNECTION=mysql +DB_HOST=db--..appwrite.center +DB_PORT=3306 +DB_DATABASE=appwrite +DB_USERNAME=appwrite +DB_PASSWORD= +``` + +Laravel's `mysql` connection drives both MySQL and MariaDB. The proxy terminates TLS for you, so the connection is encrypted without extra flags. To pin a CA for verification, set the `MYSQL_ATTR_SSL_CA` environment variable, which the default `mysql` connection passes to PDO through its `options` array: + +```env +MYSQL_ATTR_SSL_CA=/path/to/ca.pem +``` +{% /tabsitem %} +{% /tabs %} + +# Run migrations {% #migrate %} + +Define your schema with a migration: + +```php +Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->string('title'); + $table->text('body'); + $table->timestamps(); +}); +``` + +Apply migrations from your machine or a deploy step: + +```bash +php artisan migrate + +# non-interactive, for CI and production deploys +php artisan migrate --force +``` + +Run `migrate` against the **direct** engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB), not the pooler. Migrations issue DDL that needs a real session-level connection, and the transaction-mode pooler can't keep state across statements. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +# Query with Eloquent {% #query %} + +Once the schema is migrated, use Eloquent models and the query builder as usual: + +```php +use App\Models\Post; + +$post = Post::create([ + 'title' => 'Hello from a dedicated database', + 'body' => 'Stored in a managed dedicated database.', +]); + +$recent = Post::query() + ->orderByDesc('created_at') + ->limit(10) + ->get(); +``` + +Nothing about the dedicated database changes how Eloquent, relationships, transactions, or the query builder behave, it's a standard database server behind a TLS endpoint. + +# Pool connections from serverless {% #pooling %} + +The right endpoint depends on how your app runs. + +A **long-running** PHP process, traditional PHP-FPM with persistent connections, [Laravel Octane](https://laravel.com/docs/octane), or a queue worker, holds its own backend connection for its lifetime. Point these at the **direct** engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB), or at the [connection pooler](/docs/products/databases/dedicated/pooler) in **session mode**. Don't put a long-lived process behind the transaction-mode pooler. + +A **serverless** or per-request deployment (Vercel, AWS Lambda, Cloud Run) opens a fresh connection on every invocation and fans out into far more backend connections than the engine allows. Route that traffic through the pooler's **transaction-mode** port instead (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) on the same hostname, and keep migrations on the direct port: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +# Runtime: pooled, transaction mode +DB_HOST=db--..appwrite.center +DB_PORT=6432 + +# Migrations: run with DB_PORT=5432 against the direct engine port +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +# Runtime: pooled, transaction mode +DB_HOST=db--..appwrite.center +DB_PORT=6033 + +# Migrations: run with DB_PORT=3306 against the direct engine port +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +# Runtime: pooled, transaction mode +DB_HOST=db--..appwrite.center +DB_PORT=6033 + +# Migrations: run with DB_PORT=3306 against the direct engine port +``` +{% /tabsitem %} +{% /tabs %} + +The transaction-mode pooler does not keep a backend connection across statements, so server-side prepared statements, advisory locks, `LISTEN`/`NOTIFY`, and `SET LOCAL` are unavailable. If your app relies on those, use **session mode** instead, see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Queues and Horizon {% #queues %} + +A queue worker is a long-running process. `php artisan queue:work` boots once and processes jobs for its whole lifetime, holding a persistent database connection the entire time, the same applies to every worker that [Laravel Horizon](https://laravel.com/docs/horizon) supervises. Treat workers like any other long-lived process: + +- Connect them to the **direct** engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB) or the **session-mode** pooler, never the transaction-mode pooler. +- Restart workers periodically with `--max-time` or `--max-jobs` so a fresh process reclaims memory and reopens its connection. Supervisor or Horizon restarts them automatically. + +```bash +php artisan queue:work --max-time=3600 --max-jobs=500 +``` + +Each worker counts as one backend connection, so size your worker pool (and Horizon's `maxProcesses`) against the connection budget of your [specification](/docs/products/databases/dedicated). Horizon itself requires Redis for the queue backend; only your application's data connection touches the dedicated database. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and credentials. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its connection details. +2. Export them as the `DB_*` variables for the job. +3. Run `php artisan migrate --force` and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations and tests run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/metabase/+page.markdoc b/src/routes/docs/products/databases/dedicated/metabase/+page.markdoc new file mode 100644 index 00000000000..dfc3ce86eaa --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/metabase/+page.markdoc @@ -0,0 +1,128 @@ +--- +layout: article +title: Metabase +description: Connect Metabase to an Appwrite dedicated PostgreSQL or MySQL database for analytics. Create a scoped read-only connection user, add the database in Metabase admin over SSL, and build dashboards on a session-safe endpoint. +--- + +A dedicated database is a standard PostgreSQL, MySQL, or MariaDB engine, so [Metabase](https://www.metabase.com/) connects to it the same way it connects to any self-hosted database server. You add the database under **Admin settings**, point it at the host from the [Connect](/docs/products/databases/dedicated/connect) page, and Metabase syncs the schema and lets you build questions and dashboards on top of it. PostgreSQL is the recommended engine for a dedicated database. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and an [API key](/docs/advanced/platform/api-keys) with access to it. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve its host and credentials. The default database name and primary username are both `appwrite`. +{% /info %} + +# Use a read-only user {% #read-only-user %} + +Don't give Metabase the primary `appwrite` user. That user owns the database and can run DDL and writes, which a BI tool never needs. Instead, create a scoped **read-only** connection user and hand Metabase those credentials. The read-only role can only run `SELECT`, so a misconfigured question or a curious analyst can't mutate or drop anything, and you can revoke the reporting user without touching your application's primary credentials. + +# Create the read-only connection user {% #create-user %} + +Create a `readonly` connection from the API. This is the same endpoint described under [connection users](/docs/products/databases/dedicated/connect#connections); here we name the user for its analytics purpose: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "metabase_ro", + "database": "appwrite", + "role": "readonly" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//connections +``` + +The response contains the generated password for the new user: + +```json +{ + "username": "metabase_ro", + "password": "", + "database": "appwrite", + "role": "readonly" +} +``` + +Appwrite stores the password encrypted and won't show it again, so copy it now. If you lose it, delete the connection and create a new one. The `readonly` role grants `SELECT` on every table in the schema and intentionally cannot run schema changes, so Metabase can read your data but never alter it. + +# Add the database in Metabase {% #add-database %} + +In Metabase, open **Admin settings → Databases → Add a database**. Fill in the connection form with the values from the [Connect](/docs/products/databases/dedicated/connect) page and the read-only user you just created: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +Choose **PostgreSQL** as the database type: + +| Field | Value | +|-------|-------| +| Host | `db--..appwrite.center` | +| Port | `5432` | +| Database name | `appwrite` | +| Username | `metabase_ro` | +| Password | the generated password from the API response | + +Enable **Use a secure connection (SSL)**, equivalent to `sslmode=require`. For full certificate verification, set the SSL mode in Metabase's SSL options to `verify-full`; the server certificate is signed by a well-known public CA, so you don't need to supply a root certificate. See the [Network](/docs/products/databases/dedicated/network) page for details. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +Choose **MySQL** as the database type. A MariaDB dedicated database uses the same **MySQL** database type and identical connection fields. + +| Field | Value | +|-------|-------| +| Host | `db--..appwrite.center` | +| Port | `3306` | +| Database name | `appwrite` | +| Username | `metabase_ro` | +| Password | the generated password from the API response | + +Enable **Use a secure connection (SSL)**, equivalent to `ssl=true` in the connection string. +{% /tabsitem %} +{% /tabs %} + +Every dedicated database requires TLS, terminated at the edge proxy, so the connection must be encrypted. See [TLS](/docs/products/databases/dedicated/connect#tls) for the available modes. + +{% info title="Pick the right endpoint" %} +A BI tool holds long-lived sessions and can use server-side cursors to stream large result sets. Connect to the **direct** engine endpoint (`5432` for PostgreSQL, `3306` for MySQL and MariaDB) or, if you pool, the **session-mode** pooler. Don't point Metabase at the **transaction-mode** pooler (the default): it doesn't keep a backend connection across statements, which breaks cursors and prepared statements. See [Pool modes](/docs/products/databases/dedicated/pooler#modes). +{% /info %} + +Click **Save**, and Metabase verifies the connection and begins its first sync. + +# Build a question or dashboard {% #build %} + +Once the first sync finishes, your tables appear in the data picker. To build your first chart: + +1. Click **+ New → Question** and pick your dedicated database as the data source. +2. Choose a table, then add summaries (counts, sums, averages) and group by a column, for example new rows per day. +3. Switch the visualization to a line, bar, or table view and **Save** the question. +4. Add saved questions to a **Dashboard** to assemble a reporting view, and set filters to slice across cards. + +Because the connection is read-only, you can also drop into the native SQL editor for ad-hoc analysis without any risk of writing to the database. Editors that run write statements will simply be rejected by the `readonly` role. + +# How Metabase syncs your schema {% #sync %} + +After you connect, Metabase scans the database to discover tables, columns, and relationships, then keeps that metadata current on a schedule: + +- A lightweight **sync** of the schema (table and column names, types, keys) runs hourly by default. +- A more intensive **scan** of field values, used to populate filter dropdowns, runs daily by default. + +New tables and columns you add to the database appear after the next sync. To pull them in immediately, open **Admin settings → Databases → your database** and click **Sync database schema**. You can also restrict which schemas Metabase tracks and adjust the sync and scan cadence from the same screen. Refer to the [Metabase documentation](https://www.metabase.com/docs/latest/databases/sync-scan) for the full set of sync and scan options. + +# Use a branch for testing {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own host. Point a second Metabase database connection at a branch when you want to validate a new dashboard against a snapshot of production data without querying the live database, then delete the branch when you're done. Create a read-only connection user on the branch the same way, so the reporting credentials stay scoped there too. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated" title="Dedicated databases" %} +Provision a managed PostgreSQL or MySQL database and check its status. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve the host and credentials, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes and ports, and why BI tools need session or direct connections. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/nextjs/+page.markdoc b/src/routes/docs/products/databases/dedicated/nextjs/+page.markdoc new file mode 100644 index 00000000000..5025051e986 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/nextjs/+page.markdoc @@ -0,0 +1,246 @@ +--- +layout: article +title: Next.js +description: Connect a Next.js App Router application to an Appwrite dedicated database from Route Handlers, Server Actions, and Server Components, pool from serverless, and fall back to the SQL API on the Edge runtime. +--- + +A dedicated database is a standard PostgreSQL, MySQL, MariaDB, or MongoDB engine, so a [Next.js](https://nextjs.org/) App Router application talks to it through any normal driver or ORM. There's nothing Appwrite-specific in the setup, you point your driver at the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and query from the server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. Deploying to Vercel? Pair this with the [Vercel](/docs/products/databases/dedicated/vercel) guide. +{% /info %} + +# Where to connect {% #where %} + +In the App Router, every server-side execution context runs on the **Node.js runtime by default**, and the Node.js runtime can open raw TCP sockets. That means you can use a standard database driver from any of these: + +- **Route Handlers** (`app/api/.../route.ts`) for public endpoints, webhooks, and REST-style APIs. +- **Server Actions** (`'use server'` functions) for form submissions and app-internal mutations. +- **Server Components** (`async` components) for read queries that render straight into the page. + +Never import a database driver into a Client Component (`'use client'`) or ship the connection string to the browser. Keep all database access on the server. + +The one exception is the **Edge runtime** (`export const runtime = 'edge'`), which runs on a constrained environment that **cannot open TCP database sockets**. If a route opts into Edge, use the [SQL API](/docs/products/databases/dedicated/sql-api) over HTTPS instead, see [the Edge runtime section](#edge) below. + +# Environment variables {% #env %} + +Put the connection string in your environment and never commit it. For local development, use `.env.local` (Next.js loads it automatically and it's git-ignored by default): + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require&pgbouncer=true" +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MariaDB uses the same `mysql://` scheme and ports as MySQL. +{% /tabsitem %} +{% /tabs %} + +`DATABASE_URL` points at the [connection pooler](/docs/products/databases/dedicated/pooler) port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) for runtime traffic, and `DIRECT_URL` points at the engine port (`5432` for PostgreSQL, `3306` for MySQL/MariaDB) for migrations. The next section explains why. The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns; the edge proxy terminates TLS, so no extra certificate configuration is needed. For full verification (`verify-full`) or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Pool from serverless {% #pooling %} + +When you deploy to Vercel, Netlify, or any serverless platform, each invocation can spin up a fresh instance with its own connection pool. Hundreds of concurrent invocations fan out into far more backend connections than the engine allows. Route runtime traffic through the [connection pooler](/docs/products/databases/dedicated/pooler) on the pooler port so it can multiplex those instances over a small number of backend connections. + +The pooler defaults to **transaction mode**, which does not keep a backend connection across statements, so server-side prepared statements aren't available. Reserve the engine port for migrations, which need a session-level connection. The two-URL split above (`DATABASE_URL` pooled, `DIRECT_URL` direct) is exactly what an ORM like Prisma expects. + +# Use a singleton client {% #singleton %} + +Instantiate one client per module scope and reuse it across invocations, so warm serverless instances don't reconnect on every request. In development, Next.js hot-reload re-evaluates modules, which can leak connections, so cache the client on `globalThis`. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +With [postgres.js](https://github.com/porsager/postgres) (note `prepare: false` for transaction-mode pooling): + +```ts +// lib/db.ts +import postgres from 'postgres'; + +const globalForDb = globalThis as unknown as { sql?: ReturnType }; + +export const sql = + globalForDb.sql ?? + postgres(process.env.DATABASE_URL!, { + prepare: false, // required: pooler transaction mode has no server-side prepared statements + }); + +if (process.env.NODE_ENV !== 'production') globalForDb.sql = sql; +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} + +With [mysql2](https://github.com/sidorares/node-mysql2), create the pool from `mysql2/promise`. `createPool` accepts the `mysql://` connection string directly, and the `ssl=true` parameter already in it enables TLS. There's no `prepare: false` equivalent to set, `pool.query()` doesn't create server-side prepared statements (only `pool.execute()` does), so it's safe in the pooler's transaction mode: + +```ts +// lib/db.ts +import mysql from 'mysql2/promise'; + +const globalForDb = globalThis as unknown as { pool?: ReturnType }; + +export const pool = globalForDb.pool ?? mysql.createPool(process.env.DATABASE_URL!); + +if (process.env.NODE_ENV !== 'production') globalForDb.pool = pool; +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} + +The same `mysql2` package drives MariaDB, the setup is identical to MySQL: + +```ts +// lib/db.ts +import mysql from 'mysql2/promise'; + +const globalForDb = globalThis as unknown as { pool?: ReturnType }; + +export const pool = globalForDb.pool ?? mysql.createPool(process.env.DATABASE_URL!); + +if (process.env.NODE_ENV !== 'production') globalForDb.pool = pool; +``` +{% /tabsitem %} +{% /tabs %} + +Query it from a Server Component or Route Handler: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ts +// app/users/route.ts +import { sql } from '@/lib/db'; + +export async function GET() { + const users = await sql`SELECT id, email FROM users ORDER BY created_at DESC LIMIT 10`; + return Response.json(users); +} +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ts +// app/users/route.ts +import { pool } from '@/lib/db'; + +export async function GET() { + const [users] = await pool.query('SELECT id, email FROM users ORDER BY created_at DESC LIMIT 10'); + return Response.json(users); +} +``` + +`mysql2` returns a `[rows, fields]` tuple, destructure the first element. Bind parameters with `?` placeholders: `pool.query('SELECT ... WHERE id = ?', [id])`. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ts +// app/users/route.ts +import { pool } from '@/lib/db'; + +export async function GET() { + const [users] = await pool.query('SELECT id, email FROM users ORDER BY created_at DESC LIMIT 10'); + return Response.json(users); +} +``` +{% /tabsitem %} +{% /tabs %} + +# Use Prisma or Drizzle {% #orm %} + +For a typed schema, migrations, and a query builder, reach for an ORM. Both integrate with the pooled `DATABASE_URL` plus direct `DIRECT_URL` pattern above. + +- **Prisma** — set `url` to the pooled endpoint (plus `&pgbouncer=true` on PostgreSQL) and `directUrl` to the engine port, then run `prisma migrate deploy`. See the [Prisma](/docs/products/databases/dedicated/prisma) guide for the full datasource config and migration flow. +- **Drizzle** — use a pooled client (`prepare: false` on postgres.js) for runtime and the direct URL for `drizzle-kit` migrations. See the [Drizzle](/docs/products/databases/dedicated/drizzle) guide. + +{% arrow_link href="/docs/products/databases/dedicated/prisma" %} +Set up Prisma against a dedicated database +{% /arrow_link %} + +# Edge runtime: use the SQL API {% #edge %} + +If a Route Handler, Server Action, or middleware opts into the Edge runtime, it can't open a TCP socket, so no engine driver will work there: + +```ts +export const runtime = 'edge'; // no TCP sockets available +``` + +From the Edge runtime, query the database through the [SQL API](/docs/products/databases/dedicated/sql-api), an HTTPS endpoint that executes one parameterised statement and returns JSON. It's opt-in per database and accepts `SELECT` by default. Use the global `fetch` available in the Edge runtime: + +```ts +// app/edge-users/route.ts +export const runtime = 'edge'; + +export async function GET() { + const response = await fetch( + 'https://.cloud.appwrite.io/v1/compute/databases//execution', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Appwrite-Project': process.env.APPWRITE_PROJECT_ID!, + 'X-Appwrite-Key': process.env.APPWRITE_API_KEY!, + }, + body: JSON.stringify({ + sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', + bindings: ['2026-05-01T00:00:00Z', 10], + }), + }, + ); + + const { rows } = await response.json(); + return Response.json(rows); +} +``` + +The response is `{ rows, rowCount, truncated, durationMs, columns }`. Bindings are sent separately and never interpolated into the SQL string. Add `APPWRITE_PROJECT_ID` and `APPWRITE_API_KEY` (a key with the `databases.read` scope) to your environment alongside the database URLs. + +{% arrow_link href="/docs/products/databases/dedicated/sql-api" %} +Read the SQL API reference +{% /arrow_link %} + +# Local development {% #local %} + +`next dev` runs on the Node.js runtime, so a local server connects to the dedicated database over TLS exactly like production, no local database or Docker needed. Keep `DATABASE_URL` and `DIRECT_URL` in `.env.local`. + +For throwaway data in tests or experiments, create a [branch](/docs/products/databases/dedicated/branches), an instant, isolated copy with its own connection string, and point `.env.local` at it. Delete the branch when you're done. + +# Deploy {% #deploy %} + +When you deploy this app to Vercel, set the same `DATABASE_URL` (pooled) and `DIRECT_URL` (direct) as project environment variables, run your migrations in the build step, and fall back to the SQL API from any Edge Functions. The [Vercel](/docs/products/databases/dedicated/vercel) guide walks through all of it, including Preview Deployments backed by dedicated branches. + +{% arrow_link href="/docs/products/databases/dedicated/vercel" %} +Deploy to Vercel +{% /arrow_link %} + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/vercel" title="Vercel" %} +Environment variables, build-step migrations, Edge Functions, and Preview Deployments. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/prisma" title="Prisma" %} +Datasource config, pooled and direct URLs, and the migration workflow. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/sql-api" title="SQL API" %} +Query over HTTPS from the Edge runtime without a TCP connection. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/prisma/+page.markdoc b/src/routes/docs/products/databases/dedicated/prisma/+page.markdoc new file mode 100644 index 00000000000..3b57c62e213 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/prisma/+page.markdoc @@ -0,0 +1,236 @@ +--- +layout: article +title: Prisma +description: Use Prisma ORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure the datasource, run migrations against the direct endpoint, and pool through the connection pooler from serverless runtimes. +--- + +A dedicated database is a standard engine, so [Prisma ORM](https://www.prisma.io/) works against it with no Appwrite-specific configuration. You point Prisma's `datasource` at the connection string from the [Connect](/docs/products/databases/dedicated/connect) page and use Prisma Migrate, Prisma Client, and the rest of the toolchain exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Set the connection string {% #connection-string %} + +Copy the connection string from the Console **Connect** view, or fetch it from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials). Put it in your environment, never commit it: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +Prisma's `mysql` provider drives MariaDB, keep the `mysql://` scheme. +{% /tabsitem %} +{% /tabs %} + +The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns. The edge proxy terminates TLS for every dedicated database, so no extra certificate configuration is needed. For full certificate verification (`verify-full`) or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Configure the datasource {% #datasource %} + +In `prisma/schema.prisma`, set the provider and read the URL from the environment: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} +``` +{% /tabsitem %} +{% /tabs %} + +The generator and models are engine-agnostic: + +```prisma +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id @default(uuid()) + email String @unique + createdAt DateTime @default(now()) +} +``` + +Pull an existing schema with `npx prisma db pull`, or create the tables from your models with a migration below. + +# Pool connections from serverless {% #pooling %} + +Prisma opens a connection pool from each running instance. On serverless and edge platforms (Vercel, Netlify, Cloudflare) where every invocation is a fresh instance, that fans out into far more backend connections than the engine allows. Route runtime traffic through the [connection pooler](/docs/products/databases/dedicated/pooler) instead by connecting on the pooler port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) on the same hostname. + +The pooler defaults to **transaction mode**, which does not keep a backend connection across statements, so server-side prepared statements are unavailable. Point the runtime URL at the pooler port, and give Prisma a separate **direct** connection on the engine port for migrations, which need a session-level connection: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} + +Add `pgbouncer=true` to the pooled URL so Prisma skips server-side prepared statements in transaction mode: + +```env +# Runtime: pooled, transaction mode, prepared statements disabled +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require&pgbouncer=true" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` + +```prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +# Runtime: pooled, transaction mode +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:6033/appwrite?ssl=true" + +# Migrations & introspection: direct connection to the engine +DIRECT_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` +{% /tabsitem %} +{% /tabs %} + +Prisma uses `directUrl` for `migrate` and `db pull` and `url` for Prisma Client at runtime. If your application relies on prepared statements, advisory locks, `LISTEN`/`NOTIFY`, or temporary tables, switch the pooler to **session mode** instead (and drop `pgbouncer=true` on PostgreSQL), see the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Run migrations {% #migrate %} + +Generate and apply a migration in development: + +```bash +npx prisma migrate dev --name init +``` + +In CI or production, apply already-generated migrations without prompting: + +```bash +npx prisma migrate deploy +``` + +Both commands connect over `directUrl` (the engine port), so they get a real session connection and full DDL privileges. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +# Seed data {% #seed %} + +Register a seed script in `package.json`: + +```json +{ + "prisma": { + "seed": "node prisma/seed.js" + } +} +``` + +```bash +npx prisma db seed +``` + +# Query with Prisma Client {% #query %} + +```ts +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const user = await prisma.user.create({ + data: { email: 'ada@example.com' } +}); + +const recent = await prisma.user.findMany({ + orderBy: { createdAt: 'desc' }, + take: 10 +}); +``` + +On long-running servers, instantiate `PrismaClient` once and reuse it. On serverless, keep a single client per module scope so warm invocations reuse it, and rely on the pooler to absorb cold-start connection churn. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Export it as `DIRECT_URL` (and the pooled variant as `DATABASE_URL`). +3. Run `prisma migrate deploy` and your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for serverless workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/rails/+page.markdoc b/src/routes/docs/products/databases/dedicated/rails/+page.markdoc new file mode 100644 index 00000000000..28c762b29cf --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/rails/+page.markdoc @@ -0,0 +1,314 @@ +--- +layout: article +title: Rails +description: Use Ruby on Rails and ActiveRecord with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database. Configure database.yml, run migrations against the direct endpoint, and size the ActiveRecord pool. +--- + +A dedicated database is a standard engine, so [Ruby on Rails](https://rubyonrails.org/) works against it through ActiveRecord with no Appwrite-specific configuration. You point `config/database.yml` at the connection details from the [Connect](/docs/products/databases/dedicated/connect) page and use ActiveRecord, migrations, and the rest of the toolchain exactly as you would against any self-hosted PostgreSQL, MySQL, or MariaDB server. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Install the database driver {% #driver %} + +ActiveRecord talks to the database through a driver gem. Add the one for your engine to your `Gemfile`: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ruby +# Gemfile +gem 'pg' +``` + +The `pg` gem builds against `libpq`, so the PostgreSQL client headers must be available at install time. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ruby +# Gemfile +gem 'mysql2' +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ruby +# Gemfile +gem 'mysql2' +``` + +The same `mysql2` adapter drives both MySQL and MariaDB. +{% /tabsitem %} +{% /tabs %} + +Then install: + +```bash +bundle install +``` + +# Set the connection string {% #connection-string %} + +Copy the connection string from the Console **Connect** view, or fetch it from the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials). Keep it in an environment variable, never commit it: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```env +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection string Appwrite returns already enables it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```env +DATABASE_URL="mysql://appwrite:@db--..appwrite.center:3306/appwrite?ssl=true" +``` + +MariaDB uses the same `mysql://` scheme and ports as MySQL. +{% /tabsitem %} +{% /tabs %} + +The TLS parameter (`sslmode=require` for PostgreSQL, `ssl=true` for MySQL and MariaDB) is already part of the string Appwrite returns. The edge proxy terminates TLS for every dedicated database, so no extra certificate configuration is needed. For full certificate verification (`verify-full`) or mTLS, see the [Network](/docs/products/databases/dedicated/network) page. + +# Configure database.yml {% #database-yml %} + +Rails reads `DATABASE_URL` automatically. The simplest configuration points the `url` at the environment variable and lets ActiveRecord parse the host, port, database, and credentials out of it: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```yaml +# config/database.yml +production: + adapter: postgresql + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```yaml +# config/database.yml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```yaml +# config/database.yml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} +{% /tabs %} + +If you'd rather set the fields explicitly, the discrete keys map one-to-one to the values from the [Connect](/docs/products/databases/dedicated/connect#credentials) response. Use ERB to read each value from the environment so no secret lands in source control: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```yaml +# config/database.yml +production: + adapter: postgresql + host: <%= ENV["DB_HOST"] %> # db--..appwrite.center + port: <%= ENV.fetch("DB_PORT", 5432) %> + database: <%= ENV.fetch("DB_NAME", "appwrite") %> + username: <%= ENV.fetch("DB_USER", "appwrite") %> + password: <%= ENV["DB_PASSWORD"] %> + sslmode: require + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```yaml +# config/database.yml +production: + adapter: mysql2 + host: <%= ENV["DB_HOST"] %> # db--..appwrite.center + port: <%= ENV.fetch("DB_PORT", 3306) %> + database: <%= ENV.fetch("DB_NAME", "appwrite") %> + username: <%= ENV.fetch("DB_USER", "appwrite") %> + password: <%= ENV["DB_PASSWORD"] %> + sslmode: required + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` + +MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS; the connection details Appwrite returns already enable it, so no extra flags are needed. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```yaml +# config/database.yml +production: + adapter: mysql2 + host: <%= ENV["DB_HOST"] %> # db--..appwrite.center + port: <%= ENV.fetch("DB_PORT", 3306) %> + database: <%= ENV.fetch("DB_NAME", "appwrite") %> + username: <%= ENV.fetch("DB_USER", "appwrite") %> + password: <%= ENV["DB_PASSWORD"] %> + sslmode: required + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} +{% /tabs %} + +When both `DATABASE_URL` and explicit keys are present, Rails merges them. `sslmode` and `pool` can still be set in `database.yml`, so a `url`-based config can carry the TLS parameter in the string and override `pool` in the YAML. + +# Size the connection pool {% #pool %} + +ActiveRecord manages a per-process connection pool. The `pool:` value caps how many backend connections a single Rails process holds, and it defaults to `5`. It must be large enough for every thread that checks out a connection, your Puma worker threads plus any background job threads in the same process. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```yaml +production: + adapter: postgresql + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```yaml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```yaml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> +``` +{% /tabsitem %} +{% /tabs %} + +Tying `pool` to `RAILS_MAX_THREADS` keeps it aligned with Puma's thread count. Each Puma **worker** is a separate process with its own pool, so the backend connection count is roughly `pool × workers × server instances`. Keep that product within your specification's `maxConnections`, see [specifications](/docs/products/databases/dedicated/specifications). + +# Run migrations {% #migrate %} + +Generate and apply migrations the usual way: + +```bash +bin/rails db:migrate +``` + +Migrations issue DDL and need a real session-level connection, so run them against the **direct** engine endpoint (port `5432` for PostgreSQL, `3306` for MySQL/MariaDB), not the transaction-mode pooler. Point `DATABASE_URL` (or a dedicated `DATABASE_URL` for the migration step) at the engine port when you run `db:migrate`. The primary `appwrite` user owns the default database and can run schema changes; narrower [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL. + +# Use ActiveRecord {% #activerecord %} + +Once `database.yml` is configured, models work with no further setup. Define a migration and model, then query through ActiveRecord: + +```ruby +# db/migrate/20240101000000_create_users.rb +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :email, null: false + t.timestamps + end + add_index :users, :email, unique: true + end +end +``` + +```ruby +# app/models/user.rb +class User < ApplicationRecord + validates :email, presence: true, uniqueness: true +end +``` + +```ruby +User.create!(email: 'ada@example.com') + +recent = User.order(created_at: :desc).limit(10) +``` + +ActiveRecord opens connections lazily and reuses them from the pool, so a long-running Puma server keeps a small, stable set of backend connections rather than opening one per request. + +# Pooling for a long-running server {% #pooling %} + +A Rails app under Puma is a long-running process: it holds an ActiveRecord pool for its lifetime. That pairs naturally with the **direct** engine endpoint (port `5432` for PostgreSQL, `3306` for MySQL/MariaDB), sized so `pool × workers` stays within your connection budget. This is the recommended setup for a persistent server. + +If you instead route through the [connection pooler](/docs/products/databases/dedicated/pooler) to absorb spikes or many app instances, prefer **session mode**, which keeps a backend connection for the whole client session and behaves like a direct connection to ActiveRecord. The pooler defaults to **transaction mode**, which hands out a different backend connection per transaction. ActiveRecord uses server-side prepared statements by default, and those are tied to one backend connection, so on the transaction-mode pooler you must disable them: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```yaml +production: + adapter: postgresql + url: <%= ENV["DATABASE_URL"] %> # pooler host, port 6432 + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + prepared_statements: false +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```yaml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> # pooler host, port 6033 + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + prepared_statements: false +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```yaml +production: + adapter: mysql2 + url: <%= ENV["DATABASE_URL"] %> # pooler host, port 6033 + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + prepared_statements: false +``` +{% /tabsitem %} +{% /tabs %} + +See the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the mode trade-offs. Whichever runtime endpoint you choose, always run `bin/rails db:migrate` against the direct endpoint. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Export it as `DATABASE_URL` for the job. +3. Run `bin/rails db:migrate` and your test suite against the branch. +4. Delete the branch when the job finishes. + +A branch has no pooler and exposes the engine port directly, so it's exactly the kind of session-level endpoint migrations want. Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for high-concurrency workloads. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} + +For ActiveRecord and migration details beyond this guide, see the [Rails configuration guide](https://guides.rubyonrails.org/configuring.html). diff --git a/src/routes/docs/products/databases/dedicated/retool/+page.markdoc b/src/routes/docs/products/databases/dedicated/retool/+page.markdoc new file mode 100644 index 00000000000..512466c4a6c --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/retool/+page.markdoc @@ -0,0 +1,143 @@ +--- +layout: article +title: Retool +description: Connect Retool to an Appwrite dedicated PostgreSQL or MySQL database to build internal and admin tools. Create a scoped connection user, add the database resource in Retool over SSL, and choose between IP allowlisting and TLS plus credentials for Retool Cloud egress. +--- + +A dedicated database is a standard engine, so [Retool](https://retool.com/) connects to it as a native database resource, no Appwrite-specific configuration required. You point Retool at the database hostname, authenticate with a connection user, and enable SSL. From there you build queries, tables, and forms in Retool's editor to create internal dashboards and admin panels backed directly by your dedicated database. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its connection details. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the host, port, and credentials. The default database name and username are both `appwrite`. +{% /info %} + +# Create a connection user {% #connection-user %} + +Don't give Retool the primary `appwrite` user, that account owns the schema and can run DDL. Instead, create a scoped [connection user](/docs/products/databases/dedicated/connect#connections) so the tool's blast radius matches what it needs to do. + +For a read-only dashboard, create a `readonly` user, which can only `SELECT`: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "retool_ro", + "database": "appwrite", + "role": "readonly" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//connections +``` + +For an admin tool that creates, updates, and deletes records, use `readwrite` instead. It grants `SELECT`, `INSERT`, `UPDATE`, and `DELETE` but, deliberately, no schema DDL: + +```bash +curl -X POST \ + -H "X-Appwrite-Project: " \ + -H "X-Appwrite-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "retool_rw", + "database": "appwrite", + "role": "readwrite" + }' \ + https://.cloud.appwrite.io/v1/compute/databases//connections +``` + +The response contains the generated password for the new user. Appwrite stores it encrypted, so copy it now; if you lose it, delete the connection and create a new one. + +{% arrow_link href="/docs/products/databases/dedicated/connect#connections" %} +Read more about scoped connection users +{% /arrow_link %} + +# Create the database resource in Retool {% #resource %} + +In Retool, go to **Resources**, choose **Create new**, then select the resource type that matches your engine. Fill in the connection fields with your dedicated database details: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +Select **PostgreSQL** as the resource type. + +| Retool field | Value | +|----------------|----------------------------------------------------------------| +| Name | A label for the resource, for example `Appwrite production` | +| Host | `db--..appwrite.center` | +| Port | `5432` | +| Database name | `appwrite` | +| Username | `retool_ro` or `retool_rw` from the step above | +| Password | The generated password returned when you created the user | +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +Select **MySQL** as the resource type. A dedicated MariaDB database uses the same MySQL resource type, MariaDB speaks the MySQL wire protocol. + +| Retool field | Value | +|----------------|----------------------------------------------------------------| +| Name | A label for the resource, for example `Appwrite production` | +| Host | `db--..appwrite.center` | +| Port | `3306` | +| Database name | `appwrite` | +| Username | `retool_ro` or `retool_rw` from the step above | +| Password | The generated password returned when you created the user | +{% /tabsitem %} +{% /tabs %} + +Under the SSL options, enable **SSL/TLS**. The edge proxy terminates TLS for every dedicated database and the connection string Appwrite returns already enables TLS, so an encrypted connection works without supplying a certificate. If you want Retool to verify the server certificate, turn on **Reject unauthorized** and leave the **CA certificate** field empty, the server certificate is signed by a well-known public CA, see the [Network](/docs/products/databases/dedicated/network#tls) page. + +The exact field labels and SSL controls can change between Retool releases; check the [Retool docs](https://docs.retool.com/) if a field name differs. Use **Test connection** before saving to confirm the credentials and SSL settings are correct. + +# Choose the right endpoint {% #endpoint %} + +Retool keeps a long-lived pool of connections to each resource and reuses them across requests. That pattern wants a stable, session-level connection, so point Retool at one of: + +- The **direct** engine endpoint (port `5432` for PostgreSQL, `3306` for MySQL/MariaDB), the default used above. Simplest, and correct for most internal tools. +- The [connection pooler](/docs/products/databases/dedicated/pooler) in **session mode** if you want to cap how many backend connections Retool consumes. + +Do not point Retool at the pooler's default **transaction mode**. Retool's session features, prepared statements, and transactions assume a connection that persists across statements, which transaction mode does not provide. See the [pooler modes](/docs/products/databases/dedicated/pooler#modes) page for the trade-offs. + +# Allow Retool Cloud through the network {% #network %} + +If your dedicated database has an [IP allowlist](/docs/products/databases/dedicated/network#allowlist) enabled on the **Network** page, inbound connections are rejected unless their source IP is on the list. Retool Cloud connects to your database from a set of regional egress IP addresses, so you have two options: + +- **Allowlist Retool's egress IPs.** Retool publishes the outbound IP ranges its cloud uses, and they depend on the resource's outbound region. Retrieve the current list from Retool, the addresses change over time, and add each range to your database's allowlist on the Network page. Don't hard-code IPs from this guide; get the authoritative list from [Retool's IP allowlist docs](https://docs.retool.com/). +- **Rely on TLS plus credentials.** Leave the allowlist off (or open) and depend on the scoped connection user and the required TLS connection to protect the database. This is simpler to operate because there's no IP list to maintain, at the cost of not restricting by source network. + +A self-hosted Retool instance connects from your own infrastructure's IP, which you can allowlist directly. + +{% arrow_link href="/docs/products/databases/dedicated/network#allowlist" %} +Configure the IP allowlist on the Network page +{% /arrow_link %} + +# Build an admin tool {% #build %} + +Once the resource is connected, add queries in the Retool editor that run against your dedicated database. A typical CRUD admin panel uses a few queries wired to a table and a form: + +- A **read** query that selects rows for a Retool **Table** component, parameterized by the table's search and pagination state. +- An **update** query that writes edited cells back, bound to the table's "save changes" event, using the `readwrite` user. +- An **insert** query behind a **Form** component to create new records. +- A **delete** query bound to a row action button, guarded by a confirmation modal. + +Reference Retool component state in queries with bound parameters (Retool's `{{ }}` syntax) rather than string-concatenating user input into the query, so the database driver sends values separately from the statement. Combined with the scoped user, that keeps an admin panel from being turned into an arbitrary-query tool. + +Because the `readwrite` role can't alter the schema, these tools can only read and modify data, never drop tables or change columns, which is exactly the boundary you want for an internal panel operated by non-engineers. For dashboards that only display data, wire the same Table and chart components to a resource that uses the `readonly` user, so a misconfigured query can't write. + +# Use a branch for staging {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string. Point a second Retool resource at a branch to give your team a staging version of an admin tool that operates on realistic data without touching production. Create the branch, read its `connectionString`, configure a Retool resource against that host, and delete the branch when you're done. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, and the IP allowlist. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes and ports for tools that hold long-lived connections. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for staging and preview environments. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/docs/products/databases/dedicated/spring-boot/+page.markdoc b/src/routes/docs/products/databases/dedicated/spring-boot/+page.markdoc new file mode 100644 index 00000000000..9bf23ecb55b --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/spring-boot/+page.markdoc @@ -0,0 +1,266 @@ +--- +layout: article +title: Spring Boot +description: Connect a Spring Boot application to an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database with Spring Data JPA and Hibernate. Configure the JDBC datasource and HikariCP pool, map entities, and run Flyway or Liquibase migrations against the direct endpoint. +--- + +A dedicated database is a standard PostgreSQL, MySQL, or MariaDB engine, so a Spring Boot application talks to it through the engine's regular JDBC driver with no Appwrite-specific configuration. You point `spring.datasource` at the JDBC URL from the [Connect](/docs/products/databases/dedicated/connect) page, size the built-in HikariCP pool, and use Spring Data JPA, Hibernate, Flyway, or Liquibase exactly as you would against any self-hosted server. Pick your engine in the tabs below; PostgreSQL is the recommended engine for dedicated databases. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the host, port, and password. The default database name and username are both `appwrite`. +{% /info %} + +# Add the dependencies {% #dependencies %} + +A Spring Data JPA application needs the JPA starter and the JDBC driver for your engine. HikariCP ships with `spring-boot-starter-data-jpa`, so it's already on the classpath, and Spring Boot picks the driver class from the JDBC URL, so the dependency is all the driver needs. + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```text +org.springframework.boot:spring-boot-starter-data-jpa +org.postgresql:postgresql +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```text +org.springframework.boot:spring-boot-starter-data-jpa +com.mysql:mysql-connector-j +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```text +org.springframework.boot:spring-boot-starter-data-jpa +org.mariadb.jdbc:mariadb-java-client +``` +{% /tabsitem %} +{% /tabs %} + +# Configure the datasource {% #datasource %} + +Build the JDBC URL from the host and port on the [Connect](/docs/products/databases/dedicated/connect) page. The engine port is `5432` for PostgreSQL and `3306` for MySQL and MariaDB, the database is `appwrite`, and the TLS parameter is mandatory because the edge proxy terminates TLS for every dedicated database. Read the credentials from the environment, never commit them: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```yaml +spring: + datasource: + url: jdbc:postgresql://db--..appwrite.center:5432/appwrite?sslmode=require + username: appwrite + password: ${DB_PASSWORD} + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + connection-timeout: 30000 + max-lifetime: 1200000 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect +``` + +The equivalent `application.properties`: + +```ini +spring.datasource.url=jdbc:postgresql://db--..appwrite.center:5432/appwrite?sslmode=require +spring.datasource.username=appwrite +spring.datasource.password=${DB_PASSWORD} +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=2 +spring.jpa.hibernate.ddl-auto=validate +``` + +`sslmode=require` encrypts the connection and is enough for the default setup. For full certificate verification, set `sslmode=verify-full` together with `sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory`, so pgJDBC validates against the JVM's default trust store instead of looking for `~/.postgresql/root.crt`, the proxy's certificate is signed by a public CA the JVM already trusts, so there is no Appwrite-specific CA to download. +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```yaml +spring: + datasource: + url: jdbc:mysql://db--..appwrite.center:3306/appwrite?sslMode=REQUIRED + username: appwrite + password: ${DB_PASSWORD} + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + connection-timeout: 30000 + max-lifetime: 1200000 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate.dialect: org.hibernate.dialect.MySQLDialect +``` + +The equivalent `application.properties`: + +```ini +spring.datasource.url=jdbc:mysql://db--..appwrite.center:3306/appwrite?sslMode=REQUIRED +spring.datasource.username=appwrite +spring.datasource.password=${DB_PASSWORD} +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=2 +spring.jpa.hibernate.ddl-auto=validate +``` + +`sslMode=REQUIRED` encrypts the connection without validating the certificate chain and is enough for the default setup. MySQL 8.x authenticates with the `caching_sha2_password` plugin, which requires TLS, so don't drop the parameter. For full certificate verification, set `sslMode=VERIFY_IDENTITY`: Connector/J validates against the JVM's default trust store, and the proxy's certificate is signed by a public CA the JVM already trusts, so there is no Appwrite-specific CA to download. +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```yaml +spring: + datasource: + url: jdbc:mariadb://db--..appwrite.center:3306/appwrite?sslMode=trust + username: appwrite + password: ${DB_PASSWORD} + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + connection-timeout: 30000 + max-lifetime: 1200000 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate.dialect: org.hibernate.dialect.MariaDBDialect +``` + +The equivalent `application.properties`: + +```ini +spring.datasource.url=jdbc:mariadb://db--..appwrite.center:3306/appwrite?sslMode=trust +spring.datasource.username=appwrite +spring.datasource.password=${DB_PASSWORD} +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=2 +spring.jpa.hibernate.ddl-auto=validate +``` + +`sslMode=trust` encrypts the connection without validating the certificate chain and is enough for the default setup. For full certificate verification, set `sslMode=verify-full`: MariaDB Connector/J validates against the JVM's default trust store, and the proxy's certificate is signed by a public CA the JVM already trusts, so there is no Appwrite-specific CA to download. +{% /tabsitem %} +{% /tabs %} + +The [Network](/docs/products/databases/dedicated/network) page covers mTLS. + +# Size the HikariCP pool {% #pool %} + +A Spring Boot server is long-running, so it holds its HikariCP pool open for the lifetime of the process. Keep `maximum-pool-size` modest. The HikariCP guidance is that throughput peaks at a small pool, roughly `(CPU cores x 2) + 1` for the engine, not hundreds of connections. A pool that's larger than the engine can serve only queues work inside the engine and adds latency. + +Each replica of your application opens its own pool, so multiply `maximum-pool-size` by the number of instances and keep the total under the connection budget of your dedicated database [specification](/docs/products/databases/dedicated/specifications). Set `max-lifetime` a little below the engine's idle timeout so HikariCP recycles connections before the server closes them. + +# Map an entity {% #entity %} + +Define a JPA entity and a Spring Data repository as usual, nothing here is Appwrite-specific: + +```java +package com.example.demo; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + // getters and setters +} +``` + +```java +package com.example.demo; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} +``` + +Inject the repository wherever you need it and call `save`, `findById`, `findByEmail`, and the rest of the generated query methods. HikariCP hands each transaction a pooled connection and returns it on commit. + +# Run migrations {% #migrate %} + +Let a migration tool own the schema and set `ddl-auto: validate` so Hibernate checks the mapping against the live tables at startup but never alters them. Add the dependency for either tool and Spring Boot runs pending migrations automatically on boot. + +**Flyway** reads versioned scripts from `src/main/resources/db/migration` (for example `V1__init.sql`). Point Flyway at the **direct** engine endpoint (`5432` for PostgreSQL, `3306` for MySQL/MariaDB) so migrations run on a real session connection with full DDL privileges. Setting `spring.flyway.url` gives Flyway its own datasource, independent of the runtime pool: + +{% tabs %} +{% tabsitem #postgresql title="PostgreSQL" %} +```ini +spring.flyway.url=jdbc:postgresql://db--..appwrite.center:5432/appwrite?sslmode=require +spring.flyway.user=appwrite +spring.flyway.password=${DB_PASSWORD} +``` +{% /tabsitem %} + +{% tabsitem #mysql title="MySQL" %} +```ini +spring.flyway.url=jdbc:mysql://db--..appwrite.center:3306/appwrite?sslMode=REQUIRED +spring.flyway.user=appwrite +spring.flyway.password=${DB_PASSWORD} +``` +{% /tabsitem %} + +{% tabsitem #mariadb title="MariaDB" %} +```ini +spring.flyway.url=jdbc:mariadb://db--..appwrite.center:3306/appwrite?sslMode=trust +spring.flyway.user=appwrite +spring.flyway.password=${DB_PASSWORD} +``` +{% /tabsitem %} +{% /tabs %} + +**Liquibase** is equivalent: it reads a changelog from `src/main/resources/db/changelog` and accepts its own `spring.liquibase.url`, `spring.liquibase.user`, and `spring.liquibase.password` pointing at the same direct endpoint. + +The primary `appwrite` user owns the default database and can run schema changes. Scoped [connection users](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) intentionally cannot run DDL, so always migrate as `appwrite`. + +# Pooling and the connection pooler {% #pooling %} + +HikariCP is already a connection pool, so a long-running Spring Boot server should connect to the **direct** engine endpoint (`5432` for PostgreSQL, `3306` for MySQL/MariaDB) and let HikariCP manage connections. Don't route a server's traffic through the [connection pooler](/docs/products/databases/dedicated/pooler) in `transaction` mode: stacking HikariCP on top of a transaction pooler double-pools the connections and, on PostgreSQL, breaks server-side prepared statements, which the PostgreSQL JDBC driver relies on. + +If you do want the pooler in front of your server, use **session** mode, which keeps a backend connection for the whole client session and preserves prepared statements. Connect HikariCP on the pooler port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB) and keep `maximum-pool-size` small. See the [pooler](/docs/products/databases/dedicated/pooler#modes) page for the mode trade-offs. Always run Flyway or Liquibase against the direct endpoint regardless of how the runtime connects. + +# Use a branch for previews and CI {% #branches %} + +Dedicated [branches](/docs/products/databases/dedicated/branches) are instant, isolated copies of a database with their own hostname and connection string, ideal for running migrations against throwaway data in a pull-request preview or an integration-test job: + +1. Create a branch from the API and read its `connectionString`. +2. Convert it to a JDBC URL and inject it as `DB_PASSWORD` and the datasource URL for the test run. +3. Boot the application (Flyway or Liquibase applies migrations) and run your test suite against the branch. +4. Delete the branch when the job finishes. + +Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations and `@DataJpaTest` integration tests run against realistic data without touching production. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/connect" title="Connect" %} +Retrieve credentials, rotate the password, and create scoped connection users. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/pooler" title="Connection pooler" %} +Pool modes, ports, and read/write splitting for the connection pooler. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/network" title="Network" %} +TLS modes, certificate verification, mTLS, and IP allowlists. +{% /cards_item %} +{% /cards %} + +{% arrow_link href="/docs/products/databases/dedicated" %} +Back to dedicated databases overview +{% /arrow_link %} diff --git a/src/routes/docs/products/databases/dedicated/vercel/+page.markdoc b/src/routes/docs/products/databases/dedicated/vercel/+page.markdoc new file mode 100644 index 00000000000..4d5f4e8f5d9 --- /dev/null +++ b/src/routes/docs/products/databases/dedicated/vercel/+page.markdoc @@ -0,0 +1,139 @@ +--- +layout: article +title: Vercel +description: Deploy an app on Vercel against an Appwrite dedicated database, configure pooled and direct connection strings, run migrations in the build step, secure access without a static IP, and back Preview Deployments with branches. +--- + +A dedicated database is a standard PostgreSQL, MySQL, MariaDB, or MongoDB engine reachable over TLS, so any app deployed on [Vercel](https://vercel.com/), Next.js, SvelteKit, Nuxt, Remix, or a plain Node.js function, connects to it with a normal driver or ORM. This guide covers what's specific to Vercel: where the connection strings go, how to run migrations on deploy, why you don't allowlist IPs, and how to wire Preview Deployments to database branches. + +Deploying a Next.js app? Read the [Next.js](/docs/products/databases/dedicated/nextjs) guide first for where to open connections in the App Router, then return here for the Vercel specifics. + +{% info title="Before you start" %} +You'll need a dedicated database in a `ready` state and its credentials. See [Dedicated databases](/docs/products/databases/dedicated) to create one and [Connect](/docs/products/databases/dedicated/connect) to retrieve the connection string. The default database name and username are both `appwrite`. +{% /info %} + +# Set environment variables {% #env %} + +Vercel scopes environment variables to three environments, **Production**, **Preview**, and **Development**. Add two variables to your project's environment-variable settings and apply them to the environments you deploy: + +```env +DATABASE_URL="postgres://appwrite:@db--..appwrite.center:6432/appwrite?sslmode=require&pgbouncer=true" +DIRECT_URL="postgres://appwrite:@db--..appwrite.center:5432/appwrite?sslmode=require" +``` + +- `DATABASE_URL` points at the [connection pooler](/docs/products/databases/dedicated/pooler) port (`6432` for PostgreSQL, `6033` for MySQL/MariaDB). Vercel serverless functions are short-lived and each invocation can open its own pool, so runtime traffic must go through the pooler in **transaction mode** to avoid exhausting backend connections. +- `DIRECT_URL` points at the engine port (`5432`) for migrations, which need a session-level connection the transaction-mode pooler can't provide. + +The `sslmode=require` parameter is already in the string Appwrite returns; no extra certificate configuration is needed. You can also add the variables from the CLI: + +```bash +vercel env add DATABASE_URL production +vercel env add DIRECT_URL production +``` + +Treat the database password as a secret. To rotate it without redeploying the database, call the [credentials endpoint](/docs/products/databases/dedicated/connect#credentials) and update the Vercel variables with the new value. + +# Run migrations in the build step {% #migrations %} + +Vercel caches dependencies between builds, which means an ORM's `postinstall` client-generation hook may not re-run on every deploy. Generate the client and apply migrations explicitly in the **Build Command**. For Prisma: + +```bash +prisma generate && prisma migrate deploy && next build +``` + +Or move it into a `vercel-build` script in `package.json` and point Vercel's Build Command at it: + +```json +{ + "scripts": { + "vercel-build": "prisma generate && prisma migrate deploy && next build" + } +} +``` + +`prisma migrate deploy` connects over `DIRECT_URL` (the engine port) so it gets a real session connection and full DDL privileges. The primary `appwrite` user owns the default database and can run schema changes. For Drizzle, run `drizzle-kit migrate` against the direct URL in the same build step. See the [Prisma](/docs/products/databases/dedicated/prisma) and [Drizzle](/docs/products/databases/dedicated/drizzle) guides for the migration commands. + +# No static egress IP {% #no-static-ip %} + +On standard Vercel plans, serverless functions and builds send outbound traffic from a **dynamic, rotating range of IP addresses**, there is no stable static egress IP. Do not try to secure the database by allowlisting Vercel's IPs; the set changes and an allowlist would either break or have to be so wide it's meaningless. + +Secure the connection with **TLS and credentials** instead, which is exactly what the dedicated database is built for: + +- Every connection is encrypted, `sslmode=require` is already in the connection string. +- Access is gated by the generated password, which you can [rotate](/docs/products/databases/dedicated/connect#rotate) from the API at any time. +- For least privilege, issue a scoped [connection user](/docs/products/databases/dedicated/connect#connections) (`readonly` / `readwrite`) for the deployment instead of the primary `appwrite` user. + +If you require IP-based controls or `verify-full`/mTLS, those live on the [Network](/docs/products/databases/dedicated/network) page. Static egress IPs on Vercel are a separate, plan-gated Vercel networking add-on, not something the dedicated database depends on. + +# Edge Functions: use the SQL API {% #edge %} + +Vercel **Edge Functions** (and any route with `export const runtime = 'edge'`) run on a constrained runtime that **cannot open TCP database sockets**, so no engine driver works there. Query the database through the [SQL API](/docs/products/databases/dedicated/sql-api) over HTTPS instead, an opt-in endpoint that executes one parameterised statement and returns JSON. Use the Edge runtime's global `fetch`: + +```ts +export const runtime = 'edge'; + +export async function GET() { + const response = await fetch( + 'https://.cloud.appwrite.io/v1/compute/databases//execution', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Appwrite-Project': process.env.APPWRITE_PROJECT_ID!, + 'X-Appwrite-Key': process.env.APPWRITE_API_KEY!, + }, + body: JSON.stringify({ + sql: 'SELECT id, email FROM users ORDER BY created_at DESC LIMIT $1', + bindings: [10], + }), + }, + ); + + const { rows } = await response.json(); + return Response.json(rows); +} +``` + +Add `APPWRITE_PROJECT_ID` and `APPWRITE_API_KEY` (a key with the `databases.read` scope) to your Vercel environment variables. Standard Node.js serverless functions don't need this, they connect over the pooler with a normal driver. + +{% arrow_link href="/docs/products/databases/dedicated/sql-api" %} +Read the SQL API reference +{% /arrow_link %} + +# Preview Deployments with branches {% #previews %} + +Vercel creates a **Preview Deployment** for every push to a non-production branch and every pull request. Pointing those previews at production data is risky, a destructive migration or test write would hit live records. Dedicated [branches](/docs/products/databases/dedicated/branches) solve this: each is an instant, isolated copy of the database with its own hostname and connection string, started from a storage snapshot so the schema and data match the source. + +A typical per-preview flow: + +1. Create a branch from the API and read its `connectionString`. +2. Set `DATABASE_URL` (pooled) and `DIRECT_URL` (direct) for the **Preview** environment, or override them for a specific Git branch, using the branch's connection string. Branch-scoped variables override the shared Preview values, so you only set what differs. +3. Run `prisma migrate deploy` (or your migrate command) against the branch in the build step. +4. Delete the branch when the pull request is merged or closed. + +Because each preview gets its own branch, tests in one deployment never affect another or production. + +{% arrow_link href="/docs/products/databases/dedicated/branches" %} +Create and manage branches +{% /arrow_link %} + +# Deploy from an example {% #examples %} + +The fastest way to start is from a working example. Clone an Appwrite dedicated-database example for your framework (Next.js + Prisma, SvelteKit + Drizzle, and others), import the repository into Vercel, set `DATABASE_URL` and `DIRECT_URL` as described above, and deploy. Where an example provides a Deploy button, it forwards you to Vercel's import flow with the required environment-variable names pre-filled, you still paste in your own connection strings. Check the [Dedicated databases](/docs/products/databases/dedicated) overview for the current example repositories. + +# Related {% #related %} + +{% cards %} +{% cards_item href="/docs/products/databases/dedicated/nextjs" title="Next.js" %} +Where to connect in the App Router and the Edge runtime caveat. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/prisma" title="Prisma" %} +Datasource config, pooled and direct URLs, and the migration workflow. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/branches" title="Branches" %} +Ephemeral database copies for preview environments and CI. +{% /cards_item %} +{% cards_item href="/docs/products/databases/dedicated/sql-api" title="SQL API" %} +Query over HTTPS from Edge Functions without a TCP connection. +{% /cards_item %} +{% /cards %} diff --git a/src/routes/integrations/cloudflare/+page.markdoc b/src/routes/integrations/cloudflare/+page.markdoc new file mode 100644 index 00000000000..8a17be33ce1 --- /dev/null +++ b/src/routes/integrations/cloudflare/+page.markdoc @@ -0,0 +1,37 @@ +--- +layout: integration +title: Cloudflare +description: Connect Cloudflare Workers and Pages to an Appwrite dedicated database — over TCP through Hyperdrive, or over HTTPS through the SQL API. +date: 2026-06-09 +featured: false +isPartner: false +isNew: true +cover: /images/integrations/cloudflare/cover.avif +category: deployments +product: + avatar: '/images/integrations/avatars/cloudflare.avif' + vendor: Cloudflare + description: 'Cloudflare runs a global edge network with Workers, Pages, and Hyperdrive — serverless compute and database connection acceleration at the edge.' +platform: + - 'Cloud' +images: + - /images/integrations/cloudflare/cover.avif +--- + +[Cloudflare](https://www.cloudflare.com/) runs Workers and Pages on a global edge network. An Appwrite [dedicated database](/docs/products/databases/dedicated) is reachable from both, two ways depending on whether your Worker holds a TCP connection. + +# How does the integration work? + +[Hyperdrive](https://developers.cloudflare.com/hyperdrive/) pools and accelerates a TCP connection to your database's direct endpoint, so a Worker connects with `postgres.js`, `node-postgres`, or Drizzle through the Hyperdrive binding. Alternatively, the [SQL API](/docs/products/databases/dedicated/sql-api) runs parameterised SQL over HTTPS with nothing but `fetch` — ideal when you don't want to run Hyperdrive. + +# How to implement + +1. Create a dedicated database and copy its connection string ([Connect](/docs/products/databases/dedicated/connect)). +2. Either create a Hyperdrive config pointing at the direct endpoint and bind it to your Worker, or enable the [SQL API](/docs/products/databases/dedicated/sql-api). +3. Query from your Worker — see the full [**Cloudflare guide**](/docs/products/databases/dedicated/cloudflare). + +# Read more + +- [Cloudflare with dedicated databases](/docs/products/databases/dedicated/cloudflare) +- [SQL API](/docs/products/databases/dedicated/sql-api) +- [Connection pooler](/docs/products/databases/dedicated/pooler) diff --git a/src/routes/integrations/drizzle/+page.markdoc b/src/routes/integrations/drizzle/+page.markdoc new file mode 100644 index 00000000000..ca9e2d82b70 --- /dev/null +++ b/src/routes/integrations/drizzle/+page.markdoc @@ -0,0 +1,37 @@ +--- +layout: integration +title: Drizzle ORM +description: Use Drizzle ORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database — a lightweight, type-safe SQL toolkit with Drizzle Kit migrations. +date: 2026-06-09 +featured: false +isPartner: false +isNew: true +cover: /images/integrations/drizzle/cover.avif +category: databases +product: + avatar: '/images/integrations/avatars/drizzle.avif' + vendor: Drizzle + description: 'Drizzle ORM is a lightweight, type-safe TypeScript ORM with an SQL-like query builder and the Drizzle Kit migration toolkit, popular on serverless and edge runtimes.' +platform: + - 'Cloud' +images: + - /images/integrations/drizzle/cover.avif +--- + +[Drizzle ORM](https://orm.drizzle.team/) is a lightweight, type-safe TypeScript ORM. An Appwrite [dedicated database](/docs/products/databases/dedicated) is a standard SQL engine, so Drizzle connects with its normal PostgreSQL (`node-postgres`, `postgres.js`) or MySQL (`mysql2`) drivers. + +# How does the integration work? + +You pass the connection string from your dedicated database's [Connect](/docs/products/databases/dedicated/connect) view to a Drizzle driver and run Drizzle Kit for migrations. On serverless and edge runtimes you connect through the [connection pooler](/docs/products/databases/dedicated/pooler) in transaction mode, disabling prepared statements (`prepare: false` with `postgres.js`), while migrations run against the direct endpoint. + +# How to implement + +1. Create a dedicated database and copy its connection string ([Connect](/docs/products/databases/dedicated/connect)). +2. Choose a driver (`node-postgres`, `postgres.js`, or `mysql2`) and set your connection string. +3. Configure `drizzle.config.ts` and run `drizzle-kit migrate` — see the full [**Drizzle guide**](/docs/products/databases/dedicated/drizzle). + +# Read more + +- [Drizzle with dedicated databases](/docs/products/databases/dedicated/drizzle) +- [Connect to a dedicated database](/docs/products/databases/dedicated/connect) +- [Connection pooler](/docs/products/databases/dedicated/pooler) diff --git a/src/routes/integrations/nextjs/+page.markdoc b/src/routes/integrations/nextjs/+page.markdoc new file mode 100644 index 00000000000..dfdd0ce0a22 --- /dev/null +++ b/src/routes/integrations/nextjs/+page.markdoc @@ -0,0 +1,37 @@ +--- +layout: integration +title: Next.js +description: Connect a Next.js app to an Appwrite dedicated database — query from Route Handlers and Server Actions over the pooler, or from the Edge runtime over the SQL API. +date: 2026-06-09 +featured: false +isPartner: false +isNew: true +cover: /images/integrations/nextjs/cover.avif +category: deployments +product: + avatar: '/images/integrations/avatars/nextjs.avif' + vendor: Vercel + description: 'Next.js is the React framework for the web, with server-side rendering, Route Handlers, Server Actions, and both Node.js and Edge runtimes.' +platform: + - 'Cloud' +images: + - /images/integrations/nextjs/cover.avif +--- + +[Next.js](https://nextjs.org/) is the React framework for the web. An Appwrite [dedicated database](/docs/products/databases/dedicated) backs your Next.js app with managed PostgreSQL or MySQL, accessed from server code with your ORM or driver of choice. + +# How does the integration work? + +On the Node.js runtime (Route Handlers, Server Actions, Server Components) you connect with Prisma, Drizzle, or a native driver through the [connection pooler](/docs/products/databases/dedicated/pooler). On the Edge runtime, where TCP sockets aren't available, you query through the [SQL API](/docs/products/databases/dedicated/sql-api) over HTTPS. + +# How to implement + +1. Create a dedicated database and copy its connection string ([Connect](/docs/products/databases/dedicated/connect)). +2. Add a data layer with [Prisma](/docs/products/databases/dedicated/prisma) or [Drizzle](/docs/products/databases/dedicated/drizzle) and connect from server code. +3. For Edge routes, call the [SQL API](/docs/products/databases/dedicated/sql-api) — see the full [**Next.js guide**](/docs/products/databases/dedicated/nextjs). + +# Read more + +- [Next.js with dedicated databases](/docs/products/databases/dedicated/nextjs) +- [Deploy on Vercel](/docs/products/databases/dedicated/vercel) +- [SQL API](/docs/products/databases/dedicated/sql-api) diff --git a/src/routes/integrations/prisma/+page.markdoc b/src/routes/integrations/prisma/+page.markdoc new file mode 100644 index 00000000000..3c3d2088f56 --- /dev/null +++ b/src/routes/integrations/prisma/+page.markdoc @@ -0,0 +1,37 @@ +--- +layout: integration +title: Prisma +description: Use Prisma ORM with an Appwrite dedicated PostgreSQL, MySQL, or MariaDB database — type-safe queries, migrations, and seeding over a standard connection string. +date: 2026-06-09 +featured: false +isPartner: false +isNew: true +cover: /images/integrations/prisma/cover.avif +category: databases +product: + avatar: '/images/integrations/avatars/prisma.avif' + vendor: Prisma + description: 'Prisma is a next-generation ORM for Node.js and TypeScript with a type-safe query builder, schema-driven migrations, and a large ecosystem.' +platform: + - 'Cloud' +images: + - /images/integrations/prisma/cover.avif +--- + +[Prisma](https://www.prisma.io/) is a type-safe ORM for Node.js and TypeScript. An Appwrite [dedicated database](/docs/products/databases/dedicated) is a standard PostgreSQL, MySQL, or MariaDB engine, so Prisma connects to it with no Appwrite-specific configuration. + +# How does the integration work? + +You point Prisma's `datasource` at the connection string from your dedicated database's [Connect](/docs/products/databases/dedicated/connect) view. Prisma Migrate manages your schema, Prisma Client runs type-safe queries, and the [connection pooler](/docs/products/databases/dedicated/pooler) absorbs connection churn from serverless runtimes — you set a pooled `url` plus a `directUrl` for migrations. + +# How to implement + +1. Create a dedicated PostgreSQL or MySQL database and copy its connection string ([Connect](/docs/products/databases/dedicated/connect)). +2. Set `DATABASE_URL` (pooled) and `DIRECT_URL` (direct) in your environment. +3. Configure the `datasource` and run `prisma migrate` — see the full [**Prisma guide**](/docs/products/databases/dedicated/prisma). + +# Read more + +- [Prisma with dedicated databases](/docs/products/databases/dedicated/prisma) +- [Connect to a dedicated database](/docs/products/databases/dedicated/connect) +- [Connection pooler](/docs/products/databases/dedicated/pooler) diff --git a/src/routes/integrations/vercel/+page.markdoc b/src/routes/integrations/vercel/+page.markdoc new file mode 100644 index 00000000000..d229d452ddc --- /dev/null +++ b/src/routes/integrations/vercel/+page.markdoc @@ -0,0 +1,37 @@ +--- +layout: integration +title: Vercel +description: Deploy apps on Vercel backed by an Appwrite dedicated database — pooled connections for serverless functions, the SQL API for Edge Functions, and branches for Preview Deployments. +date: 2026-06-09 +featured: false +isPartner: false +isNew: true +cover: /images/integrations/vercel/cover.avif +category: deployments +product: + avatar: '/images/integrations/avatars/vercel.avif' + vendor: Vercel + description: 'Vercel is a platform for deploying and hosting frontend applications and serverless functions, with Preview Deployments and a global edge network.' +platform: + - 'Cloud' +images: + - /images/integrations/vercel/cover.avif +--- + +[Vercel](https://vercel.com/) hosts frontend apps and serverless functions. An Appwrite [dedicated database](/docs/products/databases/dedicated) gives them a managed PostgreSQL or MySQL backend reachable over the public internet. + +# How does the integration work? + +Vercel serverless functions connect through the [connection pooler](/docs/products/databases/dedicated/pooler) in transaction mode, with migrations running against the direct endpoint in the build step. Edge Functions use the [SQL API](/docs/products/databases/dedicated/sql-api) over HTTPS. Because Vercel has no static egress IP, you rely on TLS and credentials rather than IP allowlisting — and [branches](/docs/products/databases/dedicated/branches) give each Preview Deployment its own database. + +# How to implement + +1. Create a dedicated database and copy its connection string ([Connect](/docs/products/databases/dedicated/connect)). +2. Set pooled `DATABASE_URL` and direct `DIRECT_URL` as Vercel environment variables. +3. Run migrations in the build step and connect from your functions — see the full [**Vercel guide**](/docs/products/databases/dedicated/vercel). + +# Read more + +- [Vercel with dedicated databases](/docs/products/databases/dedicated/vercel) +- [Branches for Preview Deployments](/docs/products/databases/dedicated/branches) +- [SQL API](/docs/products/databases/dedicated/sql-api) diff --git a/static/images/integrations/avatars/cloudflare.avif b/static/images/integrations/avatars/cloudflare.avif new file mode 100644 index 00000000000..5704650f45a Binary files /dev/null and b/static/images/integrations/avatars/cloudflare.avif differ diff --git a/static/images/integrations/avatars/drizzle.avif b/static/images/integrations/avatars/drizzle.avif new file mode 100644 index 00000000000..1c05d66fd49 Binary files /dev/null and b/static/images/integrations/avatars/drizzle.avif differ diff --git a/static/images/integrations/avatars/nextjs.avif b/static/images/integrations/avatars/nextjs.avif new file mode 100644 index 00000000000..975e4085673 Binary files /dev/null and b/static/images/integrations/avatars/nextjs.avif differ diff --git a/static/images/integrations/avatars/prisma.avif b/static/images/integrations/avatars/prisma.avif new file mode 100644 index 00000000000..5462159ddd8 Binary files /dev/null and b/static/images/integrations/avatars/prisma.avif differ diff --git a/static/images/integrations/avatars/vercel.avif b/static/images/integrations/avatars/vercel.avif new file mode 100644 index 00000000000..0c556e9f615 Binary files /dev/null and b/static/images/integrations/avatars/vercel.avif differ diff --git a/static/images/integrations/cloudflare/cover.avif b/static/images/integrations/cloudflare/cover.avif new file mode 100644 index 00000000000..bdceeea0206 Binary files /dev/null and b/static/images/integrations/cloudflare/cover.avif differ diff --git a/static/images/integrations/drizzle/cover.avif b/static/images/integrations/drizzle/cover.avif new file mode 100644 index 00000000000..d8d60803e51 Binary files /dev/null and b/static/images/integrations/drizzle/cover.avif differ diff --git a/static/images/integrations/nextjs/cover.avif b/static/images/integrations/nextjs/cover.avif new file mode 100644 index 00000000000..fdfbd4594a9 Binary files /dev/null and b/static/images/integrations/nextjs/cover.avif differ diff --git a/static/images/integrations/prisma/cover.avif b/static/images/integrations/prisma/cover.avif new file mode 100644 index 00000000000..1469cde8073 Binary files /dev/null and b/static/images/integrations/prisma/cover.avif differ diff --git a/static/images/integrations/vercel/cover.avif b/static/images/integrations/vercel/cover.avif new file mode 100644 index 00000000000..86253dea805 Binary files /dev/null and b/static/images/integrations/vercel/cover.avif differ