Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions .agents/skills/sqlite-schema-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---
name: sqlite-schema-design
description: Design or review schemas for `crates/cloudsync` using SQLite Sync constraints, not generic SQLite advice. Use when adding synced tables, changing synced columns, or planning CloudSync-safe migrations.
---

## Goal

Design tables that behave correctly under SQLite Sync's CRDT replication model.

This skill is specifically for CloudSync-backed schemas:

- tables are initialized through `cloudsync_init(...)`
- sync is enabled with `cloudsync_enable(...)`
- identifiers should be generated with `cloudsync_uuid()`
- schema changes must go through `cloudsync_begin_alter(...)` and `cloudsync_commit_alter(...)`
- local and cloud databases must keep the same schema

Do not treat this as ordinary SQLite schema design. SQLite Sync imposes extra rules around keys, defaults, foreign keys, and schema evolution.

## Workflow

### 1. Decide Whether The Table Is Synced

Before proposing DDL, classify the table:

- synced application data: must satisfy SQLite Sync constraints
- local-only cache or ephemeral state: should usually stay out of CloudSync

Only apply this skill to synced tables or to tables that may become synced soon.

### 2. Require A Stable, Globally Unique Primary Key

For synced tables:

- always declare an explicit primary key
- prefer `TEXT PRIMARY KEY NOT NULL`
- generate ids with `cloudsync_uuid()`
- do not use auto-incrementing integer ids

SQLite Sync docs explicitly recommend UUIDv7-style globally unique ids for CRDT workloads. Integer autoincrement ids are a bad fit because multiple devices can create rows independently.

```sql
CREATE TABLE document (
id TEXT PRIMARY KEY NOT NULL DEFAULT (cloudsync_uuid()),
workspace_id TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
) STRICT;
```

If the environment cannot use a function call in `DEFAULT`, generate the id in application code, but still use `cloudsync_uuid()` as the canonical id strategy.

### 3. Make Inserts Merge-Safe With Real Defaults

SQLite Sync best practices call out a non-obvious CRDT constraint: merges can happen column-by-column, so missing values are much more dangerous than in a single-node SQLite app.

For synced tables:

- every non-primary-key `NOT NULL` column should have a meaningful `DEFAULT`
- avoid required columns that only application code knows how to populate
- prefer simple scalar defaults over nullable columns when the field is logically always present

Good:

```sql
title TEXT NOT NULL DEFAULT ''
archived INTEGER NOT NULL DEFAULT 0
sort_order INTEGER NOT NULL DEFAULT 0
```

Bad:

```sql
title TEXT NOT NULL
archived INTEGER NOT NULL
```

without defaults on a synced table.

### 4. Keep The Local And Cloud Schemas Identical

The getting-started docs require the local synced database and the SQLite Cloud database to share the same schema.

When designing or reviewing a schema:

- treat local and remote DDL as one contract
- do not introduce "client-only" columns on synced tables
- do not rely on drift being harmless
- ensure migrations are applied consistently before sync resumes

If a field is only needed locally, it likely belongs in a separate non-synced table.

### 5. Be Conservative With Foreign Keys

SQLite Sync best practices explicitly warn that foreign keys can interact poorly with CRDT replication.

Use foreign keys on synced tables only when the integrity guarantee is worth the operational cost.

If you keep them:

- make child foreign key columns nullable when the relationship is optional
- if a foreign key column has a `DEFAULT`, that default must be `NULL` or reference an actually valid parent row
- avoid fake sentinel ids such as `'root'` unless that parent row is guaranteed to exist everywhere
- index the child foreign key columns

Prefer ownership patterns that tolerate out-of-order arrival between related rows.

### 6. Avoid Triggers And Implicit Write Logic On Synced Tables

SQLite Sync best practices advise minimizing triggers because they make replicated writes harder to reason about.

For synced tables:

- avoid triggers that mutate synced columns
- avoid hidden side effects on insert or update
- prefer explicit application writes
- keep derived or bookkeeping writes in non-synced tables if possible

If a trigger is unavoidable, review it as part of the replication design, not as a local SQLite convenience.

### 7. Scope Uniqueness For RLS And Multi-Tenant Sync

The introduction docs emphasize row-level security and multi-tenant access patterns.

That changes uniqueness design:

- if the real rule is "unique per workspace/user/team", encode that as a composite constraint
- avoid globally unique business keys unless they truly span all tenants

Prefer:

```sql
UNIQUE (workspace_id, slug)
```

over:

```sql
slug TEXT UNIQUE
```

when data is tenant-scoped.

### 8. Keep Schema Changes Inside The CloudSync Alter Window

Schema changes for synced databases are not ordinary `ALTER TABLE` work. Use:

1. `cloudsync_begin_alter('table_name')`
2. perform the schema change
3. `cloudsync_commit_alter('table_name')`

Design implications:

- favor additive changes over destructive rewrites
- prefer adding columns with safe defaults
- avoid migrations that temporarily violate sync invariants
- plan rollouts so every replica can move cleanly to the new shape

When reviewing a migration plan, reject any synced-table schema change that skips the CloudSync alter flow.

### 9. Keep Sync Metadata Out Of Your Domain Schema

SQLite Sync already exposes its own metadata and helpers:

- `cloudsync_siteid()`
- `cloudsync_db_version()`
- `cloudsync_version()`
- `cloudsync_is_enabled()`

Do not duplicate these concepts as app-managed columns on synced tables unless there is a very specific product requirement.

### 10. Separate Network Lifecycle From Schema Design

The API set includes network setup and sync transport functions such as:

- `cloudsync_network_init(...)`
- `cloudsync_network_set_token(...)`
- `cloudsync_network_set_apikey(...)`
- `cloudsync_network_sync(...)`
- `cloudsync_network_has_unsent_changes()`

These matter operationally, but they are not substitutes for sound schema design.

Do not design tables that assume:

- sync is always online
- rows arrive in lockstep
- dependent rows replicate in a single transaction boundary visible to all peers

Assume offline creation, delayed delivery, retries, and independent merges.

## Design Defaults For Synced Tables

Unless the user explicitly asks otherwise:

- `TEXT PRIMARY KEY NOT NULL`
- ids generated with `cloudsync_uuid()`
- `STRICT` tables
- explicit `DEFAULT` on every non-key `NOT NULL` column
- composite uniqueness for tenant-scoped identifiers
- minimal or no triggers
- cautious foreign key usage
- separate non-synced tables for local UI/cache state

## Review Checklist

When reviewing a CloudSync schema, ask:

- Does every synced table have a globally unique primary key strategy?
- Are new rows creatable independently on multiple devices?
- Do all non-key required columns have defaults that make replicated inserts safe?
- Would the schema still behave correctly if related rows arrive out of order?
- Are foreign keys optional where replication ordering can vary?
- Are tenant-scoped uniqueness rules modeled as composite constraints?
- Does the migration plan use `cloudsync_begin_alter` / `cloudsync_commit_alter`?
- Is any local-only state incorrectly mixed into a synced table?
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ hypr-data = { path = "crates/data", package = "data" }
hypr-db-app = { path = "crates/db-app", package = "db-app" }
hypr-db-core2 = { path = "crates/db-core2", package = "db-core2" }
hypr-db-live-query = { path = "crates/db-live-query", package = "db-live-query" }
hypr-db-migrate = { path = "crates/db-migrate", package = "db-migrate" }
hypr-denoise = { path = "crates/denoise", package = "denoise" }
hypr-detect = { path = "crates/detect", package = "detect" }
hypr-device-monitor = { path = "crates/device-monitor", package = "device-monitor" }
Expand Down
30 changes: 29 additions & 1 deletion crates/cloudsync/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
https://github.com/sqliteai/sqlite-sync/releases/tag/1.0.12
Release:
- https://github.com/sqliteai/sqlite-sync/releases/tag/1.0.12

Docs:
- https://docs.sqlitecloud.io/docs/sqlite-sync-introduction
- https://docs.sqlitecloud.io/docs/sqlite-sync-getting-started
- https://docs.sqlitecloud.io/docs/sqlite-sync-best-practices
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-init
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-enable
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-disable
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-is-enabled
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-cleanup
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-terminate
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-version
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-siteid
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-db-version
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-uuid
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-begin-alter
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-commit-alter
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-init
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-cleanup
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-set-token
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-set-apikey
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-has-unsent-changes
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-send-changes
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-check-changes
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-sync
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-reset-sync-version
- https://docs.sqlitecloud.io/docs/sqlite-sync-api-cloudsync-network-logout
Loading
Loading