feat(train): the sync engine and Train's first models (ADR 0030, D14)

Train's server syncs with serverpod_offline_sync as Learn's does, and
holds three synced tables, every payload sealed on the devices:
content_key (the person's data key, sealed to the account), workout (its
type, times, name and totals, changed when edited) and workout_series
(chunks of a workout's samples, written once). A person's space goes
with them, with everything in it.

The migration only adds: the three tables, each indexed on spaceId and
cascading with its space (a series also with its workout), and the sync
engine's own tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 11:13:04 +01:00
commit 7b20851a0a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 31210 additions and 5 deletions

View file

@ -0,0 +1,318 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_train",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "content_key",
"dartName": "ContentKey",
"module": "halkyon_train",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "dataKeyId",
"fieldName": "dataKeyId",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "sealedKey",
"fieldName": "sealedKey",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "content_key_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "content_key_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "workout",
"dartName": "Workout",
"module": "halkyon_train",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "workout_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "workout_series",
"dartName": "WorkoutSeries",
"module": "halkyon_train",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "workoutId",
"fieldName": "workoutId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "workout_series_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "workout_series_fk_1",
"columns": [
"workoutId"
],
"referenceTable": "workout",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_series_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "workout_series_workout_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "workoutId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_base",
"version": "20261026140329453-assistant-budgets"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_oidc",
"version": "20261023180644988"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,446 @@
BEGIN;
--
-- Function: gen_random_uuid_v7()
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
-- License: MIT (copyright notice included on the generator source code).
--
create or replace function gen_random_uuid_v7()
returns uuid
as $$
begin
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
return encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
end
$$
language plpgsql
volatile;
--
-- ACTION CREATE TABLE
--
CREATE TABLE "content_key" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"dataKeyId" text NOT NULL,
"sealedKey" bytea NOT NULL
);
-- Indexes
CREATE INDEX "content_key_space_idx" ON "content_key" USING btree ("spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "workout" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"keyId" text,
"payload" bytea NOT NULL,
"nonce" bytea
);
-- Indexes
CREATE INDEX "workout_space_idx" ON "workout" USING btree ("spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "workout_series" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"spaceId" bigint,
"workoutId" uuid NOT NULL,
"keyId" text,
"payload" bytea NOT NULL,
"nonce" bytea
);
-- Indexes
CREATE INDEX "workout_series_space_idx" ON "workout_series" USING btree ("spaceId");
CREATE INDEX "workout_series_workout_idx" ON "workout_series" USING btree ("workoutId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_attempted_value" (
"id" bigserial PRIMARY KEY,
"fieldId" bigint NOT NULL,
"value" jsonb NOT NULL,
"projectionReason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_attempted_value__fieldId__unique_idx" ON "crdt_data_attempted_value" USING btree ("fieldId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_fields" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"columnId" bigint NOT NULL,
"nodeId" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_fields_row_column_idx" ON "crdt_data_fields" USING btree ("rowId", "columnId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_rows" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"spaceId" bigint NOT NULL,
"tblId" bigint NOT NULL,
"uuidRowId" uuid NOT NULL,
"nodeId" bigint NOT NULL,
"visibility" bigint NOT NULL DEFAULT 0
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_rows_space_tbl_row_idx" ON "crdt_data_rows" USING btree ("spaceId", "tblId", "uuidRowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_data_tombstone" (
"id" bigserial PRIMARY KEY,
"hlcDatetime" timestamp without time zone NOT NULL,
"hlcCounter" bigint NOT NULL,
"rowId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"clFlag" bigint NOT NULL,
"reason" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_data_tombstone_row_idx" ON "crdt_data_tombstone" USING btree ("rowId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_nodes" (
"id" bigserial PRIMARY KEY,
"uuidNodeId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"lastHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "crdt_nodes__uuidNodeId__unique_idx" ON "crdt_nodes" USING btree ("uuidNodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_columns" (
"id" bigserial PRIMARY KEY,
"tblId" bigint NOT NULL,
"name" text NOT NULL,
"columnType" text NOT NULL,
"dartType" text NOT NULL,
"isNullable" boolean NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_columns_table_column_idx" ON "crdt_schema_columns" USING btree ("tblId", "name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "crdt_schema_tables" (
"id" bigserial PRIMARY KEY,
"name" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "crdt_schema_tables__name__unique_idx" ON "crdt_schema_tables" USING btree ("name");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_integrity_violations" (
"id" bigserial PRIMARY KEY,
"type" text NOT NULL,
"domainTableName" text NOT NULL,
"uuidRowId" uuid NOT NULL,
"ownerSpaceUuid" uuid,
"incomingSpaceUuid" uuid NOT NULL,
"operation" text NOT NULL,
"uuidNodeId" uuid,
"crdtDataRowId" bigint,
"hlcDatetime" timestamp without time zone,
"hlcCounter" bigint,
"firstSeenAt" timestamp without time zone NOT NULL,
"lastSeenAt" timestamp without time zone NOT NULL,
"occurrences" bigint NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_integrity_violations_key_idx" ON "offline_sync_integrity_violations" USING btree ("type", "operation", "domainTableName", "uuidRowId", "ownerSpaceUuid", "incomingSpaceUuid");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_members" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"userUuid" uuid NOT NULL,
"role" text NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_member_unique_idx" ON "offline_sync_space_members" USING btree ("userUuid", "spaceId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_space_nodes" (
"id" bigserial PRIMARY KEY,
"spaceId" bigint NOT NULL,
"nodeId" bigint NOT NULL,
"lastReceivedHlc" jsonb
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_space_node_unique_idx" ON "offline_sync_space_nodes" USING btree ("spaceId", "nodeId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "offline_sync_spaces" (
"id" bigserial PRIMARY KEY,
"uuidSpaceId" uuid NOT NULL DEFAULT gen_random_uuid_v7(),
"currentNodeId" bigint
);
-- Indexes
CREATE UNIQUE INDEX "offline_sync_spaces__uuidSpaceId__unique_idx" ON "offline_sync_spaces" USING btree ("uuidSpaceId");
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "content_key"
ADD CONSTRAINT "content_key_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "workout"
ADD CONSTRAINT "workout_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "workout_series"
ADD CONSTRAINT "workout_series_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "workout_series"
ADD CONSTRAINT "workout_series_fk_1"
FOREIGN KEY("workoutId")
REFERENCES "workout"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION
DEFERRABLE INITIALLY DEFERRED;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_attempted_value"
ADD CONSTRAINT "crdt_data_attempted_value_fk_0"
FOREIGN KEY("fieldId")
REFERENCES "crdt_data_fields"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_1"
FOREIGN KEY("columnId")
REFERENCES "crdt_schema_columns"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_fields"
ADD CONSTRAINT "crdt_data_fields_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_1"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_rows"
ADD CONSTRAINT "crdt_data_rows_fk_2"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_0"
FOREIGN KEY("rowId")
REFERENCES "crdt_data_rows"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "crdt_data_tombstone"
ADD CONSTRAINT "crdt_data_tombstone_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "crdt_schema_columns"
ADD CONSTRAINT "crdt_schema_columns_fk_0"
FOREIGN KEY("tblId")
REFERENCES "crdt_schema_tables"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_members"
ADD CONSTRAINT "offline_sync_space_members_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_0"
FOREIGN KEY("spaceId")
REFERENCES "offline_sync_spaces"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
ALTER TABLE ONLY "offline_sync_space_nodes"
ADD CONSTRAINT "offline_sync_space_nodes_fk_1"
FOREIGN KEY("nodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE CASCADE
ON UPDATE NO ACTION;
--
-- ACTION CREATE FOREIGN KEY
--
ALTER TABLE ONLY "offline_sync_spaces"
ADD CONSTRAINT "offline_sync_spaces_fk_0"
FOREIGN KEY("currentNodeId")
REFERENCES "crdt_nodes"("id")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
--
-- MIGRATION VERSION FOR halkyon_train
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_train', '20261028101012641-workouts', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261028101012641-workouts', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_base
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_base', '20261026140329453-assistant-budgets', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261026140329453-assistant-budgets', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod', '20260824182259319', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_idp
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_idp', '20260924105404509', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_offline_sync
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_offline_sync', '20260914143806119', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
--
-- MIGRATION VERSION FOR halkyon_oidc
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_oidc', '20261023180644988', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261023180644988', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_core
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_core', '20260924105232991', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
COMMIT;

View file

@ -5,3 +5,4 @@
### the conflict by removing and recreating the conflicting migration.
20261028075229917-initial
20261028101012641-workouts