Ginboot Cloud / Docs / Databases

Databases

Create serverless, PostgreSQL-compatible databases in your own AWS account, connect any Postgres client to them, and browse, edit and query their data from the console. Each database is an Amazon Aurora DSQL cluster that AWS bills to your account.

Your AWS plan Amazon Aurora DSQL PostgreSQL wire protocol Updated October 2026

Overview

A Ginboot database is one Amazon Aurora DSQL cluster, created in the AWS account you connected to Ginboot. DSQL is serverless: there are no instances to size or patch, it scales with load, and it charges nothing for compute while nothing is running.

It speaks the PostgreSQL wire protocol, so psql, GUI tools and ordinary Postgres drivers connect to it. Instead of a password you present a short-lived AWS IAM token, which the console generates for you and your applications sign for themselves.

From the console you can:

  • create and delete databases,
  • get connection details and a token for any Postgres client,
  • browse tables and edit individual values,
  • run SQL in a query editor.

DSQL is close to Postgres but not identical. Most of what differs is about how schema changes and large writes are done; the limits section lists each difference with what to do about it.

Requirements

  • Your own AWS account, connected. Databases are created only in connected accounts, never in Ginboot's, so the data and the bill stay with you. Databases aren't available on the Hosted plan.
  • Template version 8 or later. Version 8 of Ginboot's CloudFormation template adds the DSQL permissions, including creating the service-linked role AWS needs before an account's first cluster. If your connection is older, the console asks you to update the stack.
  • Permissions. There are three: database:create, database:connect and database:delete. The Admin role has all three and the Developer role has create and connect. Deleting is left to admins because it destroys data. You can add any of them to a custom role.
  • A region where DSQL is available. Not every AWS region offers it yet. Create the database in the same region as the applications that use it.

AWS allows 20 single-region DSQL clusters per region per account by default. You can ask AWS for more through Service Quotas.

Create a database

  1. Open Databases in the console and choose New database.
  2. Give it a name, such as orders, and pick the AWS region.
  3. Choose Create. It shows as creating for a few minutes, then active.

Ginboot creates the cluster with deletion protection on, so it can't be deleted by mistake from the AWS console. Ginboot also tags the cluster with ginboot:managed, the organization and the database. Ginboot's role in your account may only change or delete clusters that carry that tag.

Connect

Open a database and choose Connect to see its connection details:

Host<cluster-id>.dsql.<region>.on.aws
Port · database · user5432 · postgres · admin
PasswordIAM token, valid 15 minutes
TLSsslmode=require

A token is only checked when a connection opens. A connection that's already open stays open after its token expires, up to DSQL's 60-minute connection limit. To open a new connection after that, choose New token.

# paste the token from the Connect dialog
PGPASSWORD='<token>' psql "host=<cluster-id>.dsql.<region>.on.aws port=5432 dbname=postgres user=admin sslmode=require"

From your application

Applications shouldn't store a token. They sign a fresh one each time they open a connection, using their own AWS credentials. To set that up:

  1. In the app's AWS permissions for that environment, allow dsql:DbConnectAdmin on the cluster's ARN, which the Connect dialog shows.
  2. Set DSQL_ENDPOINT to the host.
  3. Have your connection pool sign a token before each new connection. Set the pool's maximum connection lifetime to under an hour, because DSQL closes connections at 60 minutes.
// go get github.com/jackc/pgx/v5 github.com/aws/aws-sdk-go-v2/feature/dsql/auth
cfg, _ := pgxpool.ParseConfig("host=" + os.Getenv("DSQL_ENDPOINT") + " user=admin dbname=postgres sslmode=require")
cfg.MaxConnLifetime = 50 * time.Minute // DSQL ends connections at 60 minutes
cfg.BeforeConnect = func(ctx context.Context, cc *pgx.ConnConfig) error {
	token, err := auth.GenerateDBConnectAdminAuthToken(ctx, cc.Host, region, awsCfg.Credentials)
	cc.Password = token
	return err
}
pool, err := pgxpool.NewWithConfig(ctx, cfg)

The Connect dialog has the same snippet for Node.js and Python.

Tables, rows and queries

Each database's page has two tabs. Both run from Ginboot's servers as the admin role, using a short-lived token for your AWS account.

Tables

The Tables tab lists every table with its columns and types. Rows load 200 at a time, ordered by primary key.

To edit a value, click it. An edit changes exactly one row: the one with that row's full primary key. If no row matches, for example because someone else deleted it, nothing is changed and you're told so. Tables without a primary key are read-only here.

Query

The Query tab runs one statement at a time; ⌘/Ctrl + Enter runs it. A query stops after 25 seconds and shows at most 1,000 rows. For long jobs and large exports, connect a client instead.

Delete a database

Choose Delete and type the database's name to confirm. Ginboot turns off deletion protection and deletes the cluster, and every row in it, permanently. Deleting needs database:delete.

Ginboot doesn't keep a copy of your data. If you might need the data later, export it with a client before you delete.

Billing

The database runs in your AWS account, so AWS bills you for it directly. Ginboot doesn't add a charge. Databases are part of the Your AWS plan's flat $49 per organization per month.

DSQL charges for two things:

MeterWhat it measuresUS East (N. Virginia)
ComputeDistributed Processing Units (DPUs): the reading, writing and query work your statements do$8 / million DPUs
StorageData stored, one logical copy per region$0.33 / GB-month
Free each monthApplied to your AWS bill automatically100,000 DPUs + 1 GB

An idle database costs only its storage: no instance runs between queries. Rates differ by region; the Aurora DSQL pricing page has the current figures for yours.

DSQL charges for the work each statement does, not for time. Read-mostly workloads and bursty traffic usually cost little. Constant, high-volume writes such as request logs or metrics can cost more than an always-on Postgres instance. That's why our own telemetry doesn't go into DSQL.

Limits and differences from Postgres

We tested each of these against DSQL ourselves in October 2026. The limits also match AWS's published quotas. DSQL is changing quickly, so check AWS's documentation for anything added since.

AreaOn DSQLWhat to do
IndexesCREATE INDEX is refused. Only CREATE INDEX ASYNC works, and it builds in the background.Use ASYNC. A unique index doesn't enforce uniqueness until its build has finished.
Schema changesOne DDL statement per transaction, and DDL and data changes can't share a transaction.Run migrations one statement at a time, outside any transaction that writes data.
Adding columnsOnly a plain nullable column. ADD COLUMN with DEFAULT, NOT NULL or any other constraint is refused.Add the column bare, then fill existing rows in batches. DROP COLUMN works.
Changing a column's typeALTER COLUMN … TYPE is refused.Add a new column, copy the values in batches, switch the application over, then drop the old one.
Rows per transactionAt most 3,000 rows inserted, updated or deleted. In our tests, secondary index entries didn't count towards this.Batch bulk writes and deletes. A big DELETE or DROP SCHEMA … CASCADE fails until you split it.
Transaction size and ageAt most 10 MiB of changed data, and 5 minutes.Keep transactions short. Large text or JSON rows reach the size limit before the row limit.
ConcurrencyOptimistic. Two transactions writing the same rows both proceed, and the second to commit fails with SQLSTATE 40001.Retry the whole transaction on 40001, as shown below.
ConnectionsIAM tokens instead of passwords. Connections close after 60 minutes. One database, postgres, per cluster.Sign a token per connection and cap your pool's connection lifetime under an hour.

Many limits that older articles describe are gone. Foreign keys are stored and enforced (since August 2026), jsonb works as a column type with its operators (since June 2026), and INSERT … ON CONFLICT works.

Retrying conflicts

On DSQL, a conflict surfaces at COMMIT, so retry the whole transaction, not the last statement. Everything inside the retried function must be safe to run again: send emails and call other services only after it commits.

for attempt := 1; ; attempt++ {
	err := pgx.BeginFunc(ctx, pool, func(tx pgx.Tx) error {
		// every statement of the transaction, and nothing outside the database
		return nil
	})
	var pgErr *pgconn.PgError
	if errors.As(err, &pgErr) && pgErr.Code == "40001" && attempt < 5 {
		time.Sleep(time.Duration(attempt*attempt*20+rand.IntN(20)) * time.Millisecond)
		continue
	}
	return err
}

Adding a column with a default

ALTER TABLE orders ADD COLUMN status text;  -- no DEFAULT, no NOT NULL

-- then, in its own transaction, repeated until it updates 0 rows
UPDATE orders SET status = 'pending'
 WHERE id IN (SELECT id FROM orders WHERE status IS NULL LIMIT 1000);

New rows also need the value. Many ORMs write a field's default on insert themselves. If yours doesn't, set the value in your code.

Move an existing Postgres database

A plain pg_dump and restore won't load into DSQL as is. The dump creates indexes synchronously, and it loads each table in a single statement, which breaks the 3,000-row limit for any table bigger than that. Move the schema and the data separately instead.

1. Check the schema

Compare your schema with the limits above. Look for types or extensions your app relies on, and for every table without a primary key. A primary key lets you copy a table in pages and rerun the copy safely. Also look for columns your code no longer uses. ORM migrations seldom drop old columns, and there's no reason to carry them over.

2. Create the schema

Create the tables first, one statement each, then the indexes with CREATE INDEX ASYNC. Wait for the indexes to build before relying on unique ones. If an ORM manages your schema, make sure it creates everything, including many-to-many join tables, which often have no model of their own.

3. Copy the data

Copy parent tables before the tables that reference them, because DSQL enforces foreign keys. Read each table in primary-key order, a page at a time, and write each page as one transaction:

-- read from Postgres, after the last key of the previous page
SELECT id, customer_id, total FROM orders WHERE id > $1 ORDER BY id LIMIT 1000;

-- write to DSQL as an upsert, so the copy can be rerun safely
INSERT INTO orders (id, customer_id, total) VALUES ($1, $2, $3), ($4, $5, $6) -- … up to 1,000 rows
ON CONFLICT (id) DO UPDATE SET customer_id = EXCLUDED.customer_id, total = EXCLUDED.total;

Because every page is an upsert, running the copy again finishes an interrupted run and brings changed rows across. Keep pages small enough for both the 3,000-row and the 10 MiB limits, and retry a page that fails with 40001.

4. Verify and switch over

  1. Compare row counts table by table.
  2. Stop writes to the old database, run the copy once more, and check the counts again. Rows deleted from the source since the first copy show up here as differences.
  3. Point your application at DSQL. Before this step, make sure it signs IAM tokens and retries 40001.

Keep the old database until you've watched the application run on DSQL for a while.

What we learned moving our own database

We're moving Ginboot Cloud's own control-plane database, the one behind this console, from Postgres to DSQL. It holds organizations, apps, builds, deployments and permissions: 43 tables, built with the GORM ORM. So far, our test copy has moved every row into a DSQL cluster and matched every table's row count. The production switch is still ahead of us. This is what we've learned so far, as of October 2026.

  • Most of the schema work was indexes

    Our ORM's schema came to 151 statements, and DSQL refused 106 of them. All 106 were index creations, and every one worked once rewritten as CREATE INDEX ASYNC. All the tables, column types, foreign keys and jsonb columns were accepted unchanged.

  • Test the changes you'll make next year, not just today's schema

    Creating the schema from scratch was the easy part. Adding a column with a default, the most common migration we ship, is refused on DSQL. We now add such columns bare and fill existing rows in batches. That works because our ORM writes defaults on insert itself. Check that yours does the same before you rely on it.

  • Every transaction needs to be safe to retry

    We wrapped all 24 of our transactions in a retry on 40001, then checked each one. None of them sends email or calls AWS from inside the transaction, so running one again is harmless.

    Six were turning database errors into friendly messages such as "email already registered". A conflict passing through one of them became that message, which hid it from the retry. Pass conflicts through untouched.

  • Keep high-volume writes out of DSQL

    Request logs, metrics and traces are append-only, written constantly and deleted in bulk. DSQL charges for every write and limits each transaction to 3,000 rows, which makes that workload expensive and awkward. Ours go to a dedicated telemetry store, Grafana Cloud, so the database only holds what the console manages.

  • Your catalog queries may read DSQL wrong

    DSQL stores each table inside its primary-key index, and that index lists every column as an INCLUDE column. Code that treats every column in pg_index.indkey as part of the key concludes that the key is the whole row. Our copy tool did, and its first upsert failed. Only the first indnkeyatts columns are the key.

  • Ask the copy to refuse instead of guess

    Our copy stops when the target lacks a column or table the source has, unless you've told it explicitly to leave that data behind. That check found five columns our ORM had stopped using but never dropped.

    It also found a missing many-to-many join table. Our migration code built each model's table but never the join tables between models. Copying around it would have arrived with every custom role stripped of its permissions.

  • The first cluster in an account needs a service-linked role

    Before AWS creates the first DSQL cluster in an account, the caller must be allowed to create DSQL's service-linked role. That is why databases need version 8 of our CloudFormation template: version 7 didn't have that permission.