The app and the client package are under AGPL-3.0-or-later, the server under SSPL-1.0, as decided for before the first publish: the server builds on Serverpod's core, which is SSPL, while the app links only BSD-3 packages and the client is generated from our own models. "Or later" keeps the code compatible with future AGPL versions without asking every contributor. Each package carries the official text in its LICENSE (from gnu.org and the SPDX licence data) and a notice in its README; LICENSE.md explains the split and covers the files outside the packages.
CONTRIBUTING.md: contributions come under the licence of the package they change, with no copyright assignment; every commit carries a Signed-off-by certifying the Developer Certificate of Origin, quoted verbatim; tool/check.sh runs before pushes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The first migration since the initial one, created with the CLI (serverpod create-migration --tag studysets), since no MCP bridge was set up yet. On the server it is one transaction that only adds: gen_random_uuid_v7(), the studyset table, the sync engine's tables (spaces, their members and nodes, the CRDT bookkeeping, the log of integrity violations) with their foreign keys, and, from Serverpod v4.0.3, the auth module's profile image foreign key re-created as DEFERRABLE INITIALLY DEFERRED, so that dumps restore. The server tests apply it to their Postgres.
studyset gets an index on spaceId: deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables. AGENTS.md makes it the rule for every synced table.
The client migration creates each device's SQLite schema. The generator builds it from the whole definition, so it also contains the server's own tables; only the synced tables and the bookkeeping ever hold data on a device.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Moves every Serverpod package, and the four overrides for serverpod_offline_sync, from v4.0.2 to v4.0.3 before the first synced table's migration exists, so that migration is created on the version that stays.
v4.0.3 contains fixes only. Checked before moving: serverpod_database gained only backwards-compatible changes, none of which the sync package calls into. Server-side session keys are hashed as before (the cheaper argon2 applies to JWT refresh tokens, which we don't use), so no session ends. The auth module's profile image relation is now deferred, which fixes restoring a Postgres dump; step 7's migration carries that change.
argon2 now runs in serverpod_argon2 (BSD-3), a new dependency of the server only: a prebuilt, statically linked library that needs no glibc, bundled by dart build cli like the SQLite libraries, so the Docker build is unchanged. It doesn't reach the app.
The code generator still fails on Dart v3.13's primary constructors in endpoint files (tried again), so the server keeps the four syntax lints off. The regenerated test tools gain withServerpod's ephemeralDatabase option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Adopts serverpod_offline_sync v0.0.8 (ADR 0001): its module (nickname offline_sync) and the experimental database: sync in generator.yaml, its server and client packages, and overrides that keep the four Serverpod core packages on v4.0.2, since the package pins v4.0.0 exactly and the spike showed it works on v4.0.2. The app's new dependencies come along because the workspace shares one lock file.
Studyset is the first synced model. Its space owns it, so it has no owner column, and only the envelope is a column: keyId and nonce stay null (no end-to-end encryption; ADR 0002, rule 6), and payload holds the content as versioned UTF-8 JSON, so a new content field changes the payload's schema version instead of this table, whose every migration locks older app versions out of syncing (rule 3). The audience column of the original design is gone: it served key selection, and the engine's spaces decide who reads a row.
The generated server and client code is included. The migration isn't: it is created together with the move to Serverpod v4.0.3 and reviewed first, like the client's local migration (the client's lib/migrations is still empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Serverpod has no row-level security, so every endpoint enforces access itself, by three rules established here once: the person comes from the session, never from the client, and every query filters by them; someone else's row answers exactly like a missing one (NotFoundException, shared in lib/src/common); endpoints for people with an identity declare requiredScopes: {identified} instead of checking it themselves.
DevicesEndpoint is the first such endpoint: it lists your sessions and signs one of them out, for a lost phone. It uses the existing session table, so no migration. Serverpod's own helpers are unguarded (revokeSession deletes by id alone; listSessions without authUserId returns everyone's sessions), so the endpoint puts the owner check into the same query and then notifies the revoked device as revokeSession does. Each query carries its SQL as a comment.
The identified scope lives in lib/src/auth/scopes.dart. grantIdentified gives it to a person and to their existing session rows, because sessions keep the copy of the scopes they were started with; step 9 uses it.
Tests assert the refusals first: anonymous people and callers who aren't signed in are refused; the list shows only your own devices; signing out someone else's device fails exactly like an unknown id and leaves it alone; grantIdentified reaches existing sessions. The shared fixtures live in test/integration/support. AGENTS.md names the endpoint as the reference and warns about the unguarded helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
People can sign in without an account: AnonymousIdpEndpoint exposes Serverpod's anonymous identity provider. lib/src/auth/auth_config.dart holds the token managers and identity providers, shared by server.dart and the tests, so the tests check what production runs. At most 30 new anonymous accounts per IP address and hour (Serverpod's default is 100): a school class behind one address can still start together, while one abuser gets at most 720 a day. No migration: the tables for anonymous accounts and rate limiting already exist.
Tests: signing in creates a person; the 31st account from one IP address within an hour is refused. They run without database rollback, since signing in uses concurrent transactions that the rollback can't wrap, and delete what they create instead.
The server switches off the lint rules for Dart v3.13's constructor syntax: Serverpod v4.0.2's code generator can't parse it yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The workspace root's analysis_options.yaml includes leancode_lint 27 and enables its analyzer plugin (rules for blocs, Equatable and Flutter layout); the server and the app include that file, while the generated client keeps its own options. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include the root file, which was verified before settling on this layout. Most rules are warnings, casts, inference and raw types are strict, and the check treats infos as fatal too. lints and flutter_lints are no longer needed.
tool/check.sh checks formatting, runs dart analyze --fatal-infos and the server tests; .git/hooks/pre-push links to it, so every push has to pass it. AGENTS.md describes the checklist and how to install the hook in a fresh clone.
Fixes for the new rules: package imports, run() returns Future<void>, the server setup as one cascade, a primary constructor for the app widget, a const and a redundant argument. The workspace root keeps its conventional name `_` with a documented ignore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context.
The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed.
Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
apiServer points at learn.halkyon.eu (TLS terminated by Caddy). Production runs neither Insights nor the web server: without their blocks Serverpod doesn't start them. staging.yaml is removed; there is no staging instance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Private and shared data sync through serverpod_offline_sync (0.0.8 on Serverpod 4.0.2), with the nine rules the spike established, the consequences, risks and the way out. Evidence: branch spike/offline-sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Deviations from a stock `serverpod create`:
- Dropped Flutter web support, for two reasons:
- Appflowy editor had issues in the past, potentially related to the
Flutter-Web IME bridge. Upstream has also open bugs in relation to
that:
- AppFlowy-IO/appflowy-editor#1029 [Bug][Web] Unwanted space on click
and overall space bugs (open)
- AppFlowy-IO/appflowy-editor#782 [Bug] Editor auto-scrolling when
pressing space too quickly if placed inside a scrollable (open)
- Offline-first is a required feature. serverpod_offline_sync's SQLite
support in browsers, maybe via sqlite3.wasm + OPFS, is not
documented.
- SDK constraints raised to ^3.13.0, instead of ^3.12.2, so enjoy the
newest language features.
- Pinned to Flutter 3.47.4 via FVM.
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>