Skip to content

Database

Stack

LayerTechnologyNotes
Driver@capacitor-community/sqliteNative SQLite on iOS/Android; jeep-sqlite (WASM) in browser
ORMKysely 0.27.6Type-safe SQL query builder
Adaptercapacitor-sqlite-kyselyKysely dialect for Capacitor SQLite

Connection name: "nuxt". The <jeep-sqlite> element is injected into document.body by app/plugins/sqlite.ts before the database is opened.

Using the Database

Always use useSql(). Never import db directly — it bypasses the composable abstraction and makes code harder to test.

ts
// Correct
const sql = useSql();
const tenants = await sql.getTenants();

// Avoid
import { db } from "@/database";
const tenants = await db.selectFrom("tenants").selectAll().execute();

Available Methods

ts
const sql = useSql();

// Tenants
await sql.getTenants(); // → Tenant[]
await sql.saveTenant(tenant);
await sql.deleteTenant(tenantId);

// Enrollments
await sql.getEnrollments(); // → Enrollment[]
await sql.saveEnrollment(enrollment);

// Questionnaires
await sql.getQuestionnaires(); // → Questionnaire[]
await sql.saveQuestionnaire(q);

// Pending submissions
await sql.getPendingSubmits(); // → PendingSubmit[]
await sql.addPendingSubmit(submit);
await sql.deletePendingSubmit(id);
await sql.moveToSubmits(id); // Marks as successfully sent

Schema

tenants

One row per registered healthcare provider. Stores API credentials and auth tokens.

ColumnTypeDescription
tenant_idTEXT PKUnique tenant identifier
instance_idTEXTInstance within tenant
nameTEXTDisplay name
urlTEXTAPI base URL
api_keyTEXTAPI key
access_tokenTEXTCurrent JWT access token
refresh_tokenTEXTJWT refresh token
token_expires_atTEXTISO datetime of token expiry
token_issued_atTEXTISO datetime of token issuance
user_dataTEXTJSON blob with user/device info

enrollments

Patient enrollment records. Each row links a patient to a questionnaire with scheduling data.

ColumnTypeDescription
enrollment_idTEXT PKUnique enrollment ID
tenant_idTEXT FKtenants.tenant_id
instance_idTEXT
user_idTEXTPatient identifier
questionnaire_idTEXTLinked questionnaire
questionnaire_nameTEXTDisplay name
questionnaire_versionTEXTVersion string
schedule_payloadTEXTJSON — complex schedule definition
availabilityTEXTJSON — computed access windows
statusTEXTactive, expired, etc.

questionnaires

Questionnaire definitions (JSON Schema + UI Schema).

ColumnTypeDescription
questionnaire_idTEXT PK
tenant_idTEXT FK
instance_idTEXT
nameTEXTDisplay name
versionTEXTVersion string
bodyTEXTJSON blob — full JsonForms schema

pending_submits

Queue of submissions waiting to be sent. These exist until a successful API response is received.

ColumnTypeDescription
submission_idTEXT PK
tenant_idTEXT FK
instance_idTEXT
patient_idTEXT
questionnaire_idTEXT
device_idTEXT
payloadTEXTJSON form submission data
requestTEXTSerialized HTTP request
responseTEXTLast HTTP response
statusTEXTpending, failed, etc.
errorTEXTLast error message
created_atTEXTISO datetime

submits

History of successfully sent submissions (audit log). A submission is moved here from pending_submits only after a confirmed API 200.

ColumnTypeDescription
submission_idTEXT PK
tenant_idTEXT FK
questionnaire_idTEXT
enrollment_idTEXT
schedule_idTEXT
schedule_nameTEXT
payloadTEXTJSON form submission data
created_atTEXTISO datetime

Do not confuse pending_submits and submits

pending_submits = not yet sent. submits = successfully sent history. A row moves from pending → submits only after a successful API response.

Migrations

Migration files live in app/database/migrations/ and follow the naming convention:

YYYYMMDD[N]_description.migration.ts

Example: app/database/migrations/202503231_create_pending_submits_table.migration.ts

Each migration exports up (required) and down (optional):

ts
import type { Kysely } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable("example")
    .addColumn("id", "text", (col) => col.primaryKey())
    .addColumn("name", "text", (col) => col.notNull())
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable("example").execute();
}

Migrations are auto-discovered via import.meta.glob and run by Kysely's BrowserMigrator on every app startup. The migrator is idempotent — it skips already-applied migrations.

Adding a migration

  1. Create app/database/migrations/YYYYMMDD[N]_your_change.migration.ts
  2. Export up (and optionally down)
  3. No registration needed — auto-discovered at build time