> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marina.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Database

> Use Marina's managed SQL database and safe deployment migrations.

`marina.db` is a managed **PostgreSQL** database for data owned by your app.
It is separate from a workspace Connection to an existing data source. Marina
creates the database when the service is approved; no setup is required.

Your organization has one managed Postgres deployment, shared by its apps.
Each app receives its own isolated schema inside it: an app can never read,
change, or list another app's data, and redeploying an app keeps its data.

## Enable the database

Declare the versioned service in `marina.json`:

```json marina.json theme={null}
{
  "schema": 1,
  "entrypoint": "src/app.js",
  "runtime": {
    "db": "v1"
  }
}
```

Database-backed deployments can require approval for the capability, managed
resources, and migrations. Provisioning and migration happen only after a
publisher approves the exact immutable artifact and its deployment plan.

## Create the schema with migrations

Runtime code cannot run DDL. Put ordered SQL files in the flat
`marina/migrations` directory instead:

```text theme={null}
marina/migrations/
├── 001_create_expenses.sql
└── 002_add_category.sql
```

```sql marina/migrations/001_create_expenses.sql theme={null}
create table expenses (
  id text primary key,
  owner_id text not null,
  description text not null,
  amount numeric(12, 2) not null check (amount >= 0),
  created_at timestamptz not null default now()
);
```

```sql marina/migrations/002_add_category.sql theme={null}
alter table expenses add column category text;
```

Marina applies files in lexical filename order. Each file runs atomically, and
Marina records each applied file's name and checksum. Never edit, rename,
remove, or reorder an applied migration; add a new file for the next change.
An unchanged deployment is a no-op, while changed or missing history is
rejected.

Before a deployment can be approved, Marina:

1. Validates the exact migration sequence in an isolated test database.
2. Applies deterministic safety rules.
3. Adds data and operational risks to the deployment approval plan.

Automated analysis cannot override a safety blocker. Marina applies only the
migrations contained in the approved app version.

## Write expand-compatible migrations

A migration may finish before the new version becomes live, so it must remain
compatible with the currently live version. Prefer this sequence:

1. **Expand:** add nullable columns, new tables, or other compatible structures.
2. **Deploy:** update the app to tolerate both the old and new shape.
3. **Backfill:** move large, bounded data rewrites out of deployment DDL.
4. **Contract later:** remove old structures only through an explicitly reviewed
   maintenance operation, not a deployment migration.

Deployment migrations block destructive changes such as drops, truncation,
renames, column type changes, immediate `NOT NULL`, privilege or ownership
changes, dynamic `DO` blocks, and attempts to override Marina's role, search
path, timeouts, or transaction. Data rewrites, immediate constraint validation,
and potentially blocking index builds require explicit review.

Do not include `BEGIN`, `COMMIT`, `ROLLBACK`, savepoints, or prepared transaction
commands. Marina owns the transaction boundary.

| Migration limit | Database v1 |
| - | - |
| Files | 100 |
| File size | 256 KiB |
| Filename | 1–128 characters ending in `.sql` |
| Layout | Regular files directly under `marina/migrations/` |

## Query from the app

Use positional parameters for every value. Never interpolate request data into
SQL text:

```js src/app.js theme={null}
export default {
  async fetch(request, marina) {
    const userId = request.headers.get("x-platform-user-id");
    if (!userId) return new Response("Unauthorized", { status: 401 });

    if (request.method === "POST") {
      const input = await request.json();
      const result = await marina.db.query(
        `insert into expenses (id, owner_id, description, amount)
         values ($1, $2, $3, $4)
         returning id, description, amount, created_at`,
        [crypto.randomUUID(), userId, input.description, input.amount],
      );
      return Response.json(result.rows[0], { status: 201 });
    }

    const result = await marina.db.query(
      `select id, description, amount, created_at
       from expenses
       where owner_id = $1
       order by created_at desc
       limit 100`,
      [userId],
    );
    return Response.json(result.rows);
  },
};
```

Each call returns `{ rows, rowCount }`. Values crossing the runtime boundary
are JSON-compatible. Timestamps are represented as ISO strings, large integers
and exact numerics can be strings, and binary values are represented as
`{ $bytes: "<base64>" }`.

<Note>
  The private app schema isolates applications, not users within one app. Put the authenticated user
  or tenant in the data model and include it in every relevant query predicate.
</Note>

## Run an atomic transaction

Pass a bounded list of parameterized statements when they must all commit or
all roll back:

```ts theme={null}
const [debit, credit] = await marina.db.transaction([
  {
    text: "update accounts set balance = balance - $1 where id = $2",
    params: [10, "source"],
  },
  {
    text: "update accounts set balance = balance + $1 where id = $2",
    params: [10, "destination"],
  },
]);
```

Interactive transaction callbacks are intentionally unavailable. Do not make
network calls between transaction statements; compute the complete list first.

## Runtime limits and query guidelines

| Limit | Database v1 |
| - | - |
| SQL text per statement | 100 KiB |
| Parameters per statement | 100 |
| Serialized parameters | 1 MiB |
| Statements per transaction | 25 |
| Statement execution | 10 seconds |
| Rows returned per statement | 1,000 |
| Serialized operation result | 5 MiB |
| One serialized result row | 2 MiB |

Use explicit column lists, predicates, and `limit` clauses. Paginate large
reads and keep long-running backfills out of request handlers. Runtime access
allows table `SELECT`, `INSERT`, `UPDATE`, and `DELETE` plus sequence use; it
does not allow schema creation or DDL.

## Handle conflicts and availability

Runtime failures have the name `MarinaRuntimeError`, a stable `code`, and a
`retryable` hint:

```js theme={null}
try {
  await marina.db.query(
    "insert into expenses (id, owner_id, description, amount) values ($1, $2, $3, $4)",
    [id, userId, description, amount],
  );
} catch (error) {
  if (error?.name === "MarinaRuntimeError" && error.code === "CONFLICT") {
    // Return a conflict or retry only when error.retryable is true.
  }
  throw error;
}
```

Use bounded exponential backoff only when `retryable` is true. Do not expose
raw SQL, parameters, or database errors in an HTTP response or application log.

## Data lifecycle and restores

The schema belongs to the app identity, not one artifact version. Redeploying,
renaming, or archiving the app keeps its data. Deleting an app does not
automatically drop its schema; database destruction requires a separate
explicit data-deletion workflow.

Marina does not run down migrations. A version can be restored only when the
database schema revision recorded on that version exactly matches the app's
current revision. After any migration changes the revision, Marina refuses a
restore of an older revision. Deploy new code that is compatible with the
current schema instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.