Last updated 2026-09-04Development preview

Databases

Understand servers, logical databases, runtime credentials, migrations, and the boundary between local PostgreSQL and Cloud SQL.


A relational database stores structured records and lets your application query them with SQL. Fluffy prepares the database and the runtime's access to it. Your SQL driver still owns connections and queries, and your application still owns the schema.

There are three separate decisions: declare where the data belongs, grant each runtime the access it needs, and arrange for schema changes to run before the application starts using them.

#Understand database access

#A server contains databases

A database server is the running PostgreSQL service. It can contain several logical databases. Databases under one declared server share its physical instance, capacity, availability, and recovery policy.

Inside a database, your migrations create tables and other schema objects. Declaring a logical database does not tell Fluffy what columns your application needs.

#A runtime receives its own access

A runtime can have read, write, or both actions for a named database. Another runtime does not inherit those grants. These actions control database access; they do not make the runtime a database administrator.

Both read and write grants generate GetConnection. The generated method name stays the same because it supplies a connection, not a SQL operation. The database identity's permissions determine what queries can succeed. A read-only connection is not made writable by typing an INSERT statement.

#Your driver runs the queries

The generated database client obtains connection coordinates from the sidecar. A separate Credential call obtains the credential used to authenticate a new connection. Your ordinary PostgreSQL driver then connects to the database and executes SQL.

Open one pool per database as appropriate for your application. Obtain a fresh credential before each new physical connection, and set deadlines for startup and queries. A connection pool can outlive any one login token; do not treat a token captured at startup as a permanent password.

#Migrations and seeds have different jobs

A migration changes the schema or moves existing data into a new form. A seed creates data useful for local development. A migration must make sense for existing application data; a local seed may be disposable.

Fluffy runs the declared database lifecycle tasks before starting workloads. A failure stops startup rather than starting the new application against an unprepared database. Your migration tool determines versioning, transactions, and rollback behavior.

#Configure a database

#Declare a PostgreSQL database

In fluffy-chainsaw.yaml, declare a server named primary with a logical database named records:

yaml
databaseServers:
  primary:
    minimumMajorVersion: 17
    databases:
      records: {}

minimumMajorVersion is a compatibility requirement, not an instruction to install an arbitrary PostgreSQL release. The selected product must satisfy it. If your schema needs a PostgreSQL extension, declare it under that database's requiredExtensions, for example [citext], so Fluffy can check support.

#Configure provider policy

This workspace excerpt defines a Cloud SQL type and selects it for the prod environment:

yaml
resourceTypes:
  databaseServers:
    postgres:
      product: gcp.cloud-sql.postgres
      sizes:
        standard:
          tier: db-custom-1-3840
          diskGb: 20
      addons:
        gcp:
          availabilityType: ZONAL
          pointInTimeRecovery: true
environments:
  prod:
    databaseServers:
      defaults:
        type: postgres
        provider: application
        size: standard

Add this to fluffy-chainsaw.workspace.yaml alongside an existing application provider with its project and region. The application names the logical server; the workspace chooses how to supply it. All servers using this type receive its policy unless environment settings override it.

#Connect your runtime to the database

Add this grant to the existing api runtime in fluffy-chainsaw.yaml:

yaml
runtimes:
  api:
    uses:
      databases:
        - name: records
          actions: [read, write]

Generate that runtime's SDK. For an application named example-app:

fluffy-chainsaw generate --application example-app --runtime api --language typescript

The Go package includes NewDatabasesRecordsClient(connection), GetConnection, and Credential. Use the SDK connection recipe for the sidecar connection. The complete database example shows a pgxpool integration that refreshes the credential for each new connection.

Keep queries parameterized and authorize the user before querying private data. Runtime database permissions do not implement your application's per-user access policy.

#Run migrations and local seeds

A migration is a command in a declared artifact. Add it to the target logical database:

yaml
databaseServers:
  primary:
    databases:
      records:
        migrations:
          artifact: api-image
          command: [/app, migrate]
          timeoutSeconds: 900

The image must actually implement /app migrate and exit successfully when the database is ready. This is a manifest excerpt, not an implementation of the command. Fluffy supplies DATABASE_URL scoped to that lifecycle task's database. Migration code uses that variable; ordinary runtime code obtains access through its generated client.

A local seed belongs in the application's fluffy-chainsaw.local.yaml:

yaml
apiVersion: fluffy-chainsaw.dev/local/v1
databases:
  records:
    seed:
      artifact: api-image
      command: [/app, seed]

Your image must also implement the seed command. Make repeated local runs predictable: use idempotent inserts or your seed tool's reset/version behavior. The complete example provides both commands and a handler to read their result.

#Verify locally

Run the application through fluffy-chainsaw local. Confirm the migration succeeds before the API starts. Insert a known record, query it, and check the returned value. Restart the local run and confirm the schema setup can run again as intended.

Test a denied operation too: a runtime with only read should not be able to write rows. Distinguish a SQL permission error from a missing table, an expired credential, or a connection failure.

#Enable database backups and point-in-time recovery

Fluffy enables automated backups for managed Cloud SQL instances. There is no application backup: true field. Point-in-time recovery is an additional setting under the workspace type's addons.gcp.pointInTimeRecovery.

Database recovery includes an interactive recovery timeline, the exact configuration, supported combinations, verification steps, and the provider restore procedure.

#Restore a database

Recovery is a provider operation. A Cloud SQL point-in-time restore creates a new instance; it does not silently replace the instance managed by your existing Fluffy deployment. See Google’s recovery procedure. Follow Database recovery to choose and verify a recovery point and plan the application cutover.

#Obtain connection details in your language

The generated database client supplies connection information. Your ordinary PostgreSQL driver owns SQL and pooling. Use the sidecar setup for your language. This example uses a database named records; use the constructor for your own declared database.

import { Metadata } from '@grpc/grpc-js'
import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as databasesv1 from './.fluffy/generated/runtimes/api/typescript/fluffy/databases/v1/databases.js'

const client = new runtimev1.DatabasesRecordsClient(address, transport)
const details = await new Promise<databasesv1.GetConnectionResponse>((resolve, reject) => {
  client.getConnection(
    databasesv1.GetConnectionRequest.fromPartial({}),
    new Metadata(),
    { deadline: Date.now() + 5000 },
    (error, value) => (error ? reject(error) : resolve(value)),
  )
})
// Pass details.url to your PostgreSQL driver with the credential obtained below.

GetConnection is not a query operation and its URL is not a credential cache. Call the same client's Credential RPC with CredentialRequest each time your SQL pool opens a physical connection; use the returned value as the password. Preserve the generated URL's TLS parameters. The complete database example demonstrates that pool hook. Do not print either the credential or a URL after inserting credentials.

#Reference: database configuration

Application fieldMeaning and constraint
databaseServers.<server>.typeOptional explicit resource type; otherwise selected by workspace policy
databaseServers.<server>.minimumMajorVersionPositive minimum acceptable PostgreSQL major version
databaseServers.<server>.existingReferences an existing server when true; requires environment binding
databaseServers.<server>.databasesNamed logical databases on this server
databases.<database>.requiredExtensionsPostgreSQL extensions required by that database; product support is checked
databases.<database>.migrationsOne lifecycle task: artifact and command required; timeout defaults to 900 seconds, allowed range 1–3600
runtimes.<runtime>.uses.databasesNamed database grants with read and/or write actions

The databases.<database> rows are relative to the server declaration, not top-level application keys. Local seed configuration is in the separate local manifest.

#Supported lifecycle boundaries

Managed servers can have Fluffy-run database setup and migration tasks. An existing: true server is different: its lifecycle and database preparation belong to its owner. Referenced databases and extensions must already exist and satisfy the declarations; managed migration tasks are not allowed there.

Runtime credentials are separate from migration/admin credentials. Fix a missing runtime grant at the grant; do not solve an application permission error by handing the runtime database administration rights.

#Local and cloud differences

BehaviorLocal PostgreSQLManaged GCP Cloud SQL
Server processDocker serviceCloud SQL instance selected by workspace policy
Runtime credentialEmpty credential within the isolated local networkNative credential obtained for new connections
Schema migrationBefore local workload startupDuring deployment before application activation
Local seedRuns when declared locallyNot part of production deployment
Automated cloud backups and PITRNot implemented by local persistenceManaged backup defaults and selected PITR policy

#Deployment and errors

Deploy using Deploy to GCP. Database administration uses a Cloud Run Job; if the deployment reports a permission such as run.jobs.run, follow Database administration permission.

A missing generated client points to the target, grant, or generation step. A missing table points to schema preparation. A denied SQL operation points to the database role and grant. A connection failure requires the provider error and connection path, not a broader grant guessed in response to every failure.

Removing a managed declaration does not imply destructive deletion. Review the retained-resource behavior and the proposed deployment change before changing ownership or topology.