2026-09-27 15:58:44 +02:00
|
|
|
# Configuration for the production instance learn.halkyon.eu.
|
feat(assistant): Claude reads studysets through a connector on the instance
A Claude plan can't drive a chat inside another app, but Claude's apps
(every plan, the phone apps too) can add a remote MCP server as a custom
connector. The instance now is one (ADR 0006): Serverpod's web server
serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no
sessions) with five read-only tools over the person's own synced rows
(their studysets, a studyset's cards and notes a page at a time, a
search across cards, a note, their progress), and the OAuth the
specification asks for: protected resource and authorization server
metadata, dynamic client registration, authorization code with PKCE
(S256 only), tokens bound to the resource, iss in the response, refresh
tokens that rotate. Caddy forwards these paths alone to the web server,
which production now runs on 8082.
People connect with a code from the app (Settings › Assistant › Connect
Claude): eight Crockford characters, valid for ten minutes and once,
typed on the authorization page, which names where it returns to, loads
nothing from elsewhere and may not be framed. Anonymous people can
connect too. Codes and tokens are kept only as SHA-256 hashes; an access
token lives an hour, a refresh token 30 days. The app lists what is
connected and disconnects it, which ends its tokens at once; deleting
the account deletes it; a daily job forgets what expired and clients
nobody connected within a week. InstanceInfo says where the connector is
(additive); the migration only adds server-only tables, so devices and
older apps are untouched.
The handlers take plain requests, so the integration tests drive the
whole flow (registration, the code, tokens, refresh, the tools, another
person's content, disconnecting, expiry, pruning, account deletion)
without a web server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 12:13:00 +02:00
|
|
|
# Caddy terminates TLS on 443 and forwards to the API server on 8080, and the
|
feat(assistant)!: no hand-off and no connector; the field searches the web (ADR 0022)
The developer decided on 23 Oct to drop the hand-off to an AI assistant
outside the app, and Claude's connector with it, in v0.19.0, the app and
the server together: build 22 raises minimumAppBuild anyway, so no app
calling the connector's endpoints uses the instance after it. An instance
is to have one way to an assistant, its operator's (ADR 0021, proposed).
The app: the field at the foot of Home, and the button in the Library, on
a studyset and while studying, search the web alone, through the engine
chosen in Settings or Android's own search, with the parts of a card the
person picks; a studyset's name and description are never offered. Gone:
the assistants a question opened in (AiAssistant, AssistantHandOff), the
switch between them and the web, Settings' choice of assistant, Connect
Claude with its route, cubit, repository and gateway, and their texts. A
settings file from before keeps the old keys until the next change of a
setting writes it anew; nothing reads them.
The server: lib/src/assistant/ (MCP, its OAuth, the five tools, connect
codes and tokens), AssistantEndpoint, the daily forgetting of the
connector's secrets (its scheduled call cancelled at start), its lines in
the deletion hook, instance.info().assistantConnectorUrl (nullable, so an
older app reads it as none), and Caddy's forwarding of /mcp, /oauth/* and
/.well-known/oauth-*. The migration drops the connector's five tables,
children first:
DROP TABLE "assistant_token" CASCADE; -- tokens of a connection
DROP TABLE "assistant_grant" CASCADE; -- OAuth codes of a connection
DROP TABLE "assistant_connection" CASCADE; -- a person's connected client
DROP TABLE "assistant_connect_code" CASCADE;
DROP TABLE "assistant_client" CASCADE; -- OAuth clients registered
No other table references them, so CASCADE drops only their own foreign
keys. For the minute between the maintenance run and the switch, the old
server answers the connector with errors and fails to delete an account
(its hook still deletes from these tables); ADR 0022 says so.
BREAKING CHANGE: a connected Claude gets no answer from the deploy on, and
the app no longer opens a question in an AI assistant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:33:53 +02:00
|
|
|
# web server's paths to it on 8082; Insights doesn't run.
|
2026-09-16 19:24:59 +02:00
|
|
|
|
|
|
|
|
# Configuration for the main API server.
|
|
|
|
|
apiServer:
|
|
|
|
|
port: 8080
|
2026-09-27 15:58:44 +02:00
|
|
|
publicHost: learn.halkyon.eu
|
2026-09-16 19:24:59 +02:00
|
|
|
publicPort: 443
|
|
|
|
|
publicScheme: https
|
|
|
|
|
|
2026-09-27 15:58:44 +02:00
|
|
|
# No insightsServer: without this block Serverpod doesn't start Insights at all (it
|
|
|
|
|
# would otherwise listen on every interface). To use Insights later, add the block
|
|
|
|
|
# back (port 8081), set `serviceSecret` in the production passwords, and reach the
|
|
|
|
|
# port through an SSH tunnel, never through Caddy.
|
feat(assistant): Claude reads studysets through a connector on the instance
A Claude plan can't drive a chat inside another app, but Claude's apps
(every plan, the phone apps too) can add a remote MCP server as a custom
connector. The instance now is one (ADR 0006): Serverpod's web server
serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no
sessions) with five read-only tools over the person's own synced rows
(their studysets, a studyset's cards and notes a page at a time, a
search across cards, a note, their progress), and the OAuth the
specification asks for: protected resource and authorization server
metadata, dynamic client registration, authorization code with PKCE
(S256 only), tokens bound to the resource, iss in the response, refresh
tokens that rotate. Caddy forwards these paths alone to the web server,
which production now runs on 8082.
People connect with a code from the app (Settings › Assistant › Connect
Claude): eight Crockford characters, valid for ten minutes and once,
typed on the authorization page, which names where it returns to, loads
nothing from elsewhere and may not be framed. Anonymous people can
connect too. Codes and tokens are kept only as SHA-256 hashes; an access
token lives an hour, a refresh token 30 days. The app lists what is
connected and disconnects it, which ends its tokens at once; deleting
the account deletes it; a daily job forgets what expired and clients
nobody connected within a week. InstanceInfo says where the connector is
(additive); the migration only adds server-only tables, so devices and
older apps are untouched.
The handlers take plain requests, so the integration tests drive the
whole flow (registration, the code, tokens, refresh, the tools, another
person's content, disconnecting, expiry, pruning, account deletion)
without a web server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 12:13:00 +02:00
|
|
|
|
feat(assistant)!: no hand-off and no connector; the field searches the web (ADR 0022)
The developer decided on 23 Oct to drop the hand-off to an AI assistant
outside the app, and Claude's connector with it, in v0.19.0, the app and
the server together: build 22 raises minimumAppBuild anyway, so no app
calling the connector's endpoints uses the instance after it. An instance
is to have one way to an assistant, its operator's (ADR 0021, proposed).
The app: the field at the foot of Home, and the button in the Library, on
a studyset and while studying, search the web alone, through the engine
chosen in Settings or Android's own search, with the parts of a card the
person picks; a studyset's name and description are never offered. Gone:
the assistants a question opened in (AiAssistant, AssistantHandOff), the
switch between them and the web, Settings' choice of assistant, Connect
Claude with its route, cubit, repository and gateway, and their texts. A
settings file from before keeps the old keys until the next change of a
setting writes it anew; nothing reads them.
The server: lib/src/assistant/ (MCP, its OAuth, the five tools, connect
codes and tokens), AssistantEndpoint, the daily forgetting of the
connector's secrets (its scheduled call cancelled at start), its lines in
the deletion hook, instance.info().assistantConnectorUrl (nullable, so an
older app reads it as none), and Caddy's forwarding of /mcp, /oauth/* and
/.well-known/oauth-*. The migration drops the connector's five tables,
children first:
DROP TABLE "assistant_token" CASCADE; -- tokens of a connection
DROP TABLE "assistant_grant" CASCADE; -- OAuth codes of a connection
DROP TABLE "assistant_connection" CASCADE; -- a person's connected client
DROP TABLE "assistant_connect_code" CASCADE;
DROP TABLE "assistant_client" CASCADE; -- OAuth clients registered
No other table references them, so CASCADE drops only their own foreign
keys. For the minute between the maintenance run and the switch, the old
server answers the connector with errors and fails to delete an account
(its hook still deletes from these tables); ADR 0022 says so.
BREAKING CHANGE: a connected Claude gets no answer from the deploy on, and
the app no longer opens a question in an AI assistant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:33:53 +02:00
|
|
|
# The web server serves profile pages (ADR 0011), the dictionaries devices
|
feat(base): SimpleLogin's forwarding page and Learn's App Links statement (ADR 0020, step 1)
SimpleLogin takes only https redirect addresses, each registered exactly,
so the instance's SimpleLogin app registers one page on the register's
web server, /simplelogin-callback, which sends the browser on to the app
that began the sign-in: a 303 to the desktop's loopback (127.0.0.1, a port
per app) or the phone's scheme, chosen by the marker the state begins with
(`<app>-<platform>.<rest>`, the rest 1 to 512 characters of base64url),
from a fixed table, so it sends nobody anywhere else. It forwards the code
or SimpleLogin's error (`deny`) with the state and nothing else, refuses
anything malformed, repeated or not a GET with a page that repeats nothing
it was given, and keeps and logs nothing (no-store, no referrer). It is
built ahead of the sign-in itself so development can sign in through
learn.halkyon.eu's page, which the development app registers too.
Android's assetlinks.json for Halkyon Learn signed with the release key
is answered by the Caddyfile on learn.halkyon.eu alone: the one domain the
project's builds name, and the server knows no app's key. A root test
holds its fingerprint to tool/release/android-certificate.sha256. The
app's intent filter comes with the sign-in; iOS's association waits for
an Apple team ID.
The profile page's language choice and look move to common/web_page.dart,
shared by both pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:27:56 +02:00
|
|
|
# download (ADR 0012), SimpleLogin's forwarding page (ADR 0020) and the calls
|
|
|
|
|
# between the instance's services (ADR 0016). Caddy forwards the paths of the
|
|
|
|
|
# first three alone to it (deploy/Caddyfile), never the last; everything else
|
|
|
|
|
# stays on the API server.
|
feat(assistant): Claude reads studysets through a connector on the instance
A Claude plan can't drive a chat inside another app, but Claude's apps
(every plan, the phone apps too) can add a remote MCP server as a custom
connector. The instance now is one (ADR 0006): Serverpod's web server
serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no
sessions) with five read-only tools over the person's own synced rows
(their studysets, a studyset's cards and notes a page at a time, a
search across cards, a note, their progress), and the OAuth the
specification asks for: protected resource and authorization server
metadata, dynamic client registration, authorization code with PKCE
(S256 only), tokens bound to the resource, iss in the response, refresh
tokens that rotate. Caddy forwards these paths alone to the web server,
which production now runs on 8082.
People connect with a code from the app (Settings › Assistant › Connect
Claude): eight Crockford characters, valid for ten minutes and once,
typed on the authorization page, which names where it returns to, loads
nothing from elsewhere and may not be framed. Anonymous people can
connect too. Codes and tokens are kept only as SHA-256 hashes; an access
token lives an hour, a refresh token 30 days. The app lists what is
connected and disconnects it, which ends its tokens at once; deleting
the account deletes it; a daily job forgets what expired and clients
nobody connected within a week. InstanceInfo says where the connector is
(additive); the migration only adds server-only tables, so devices and
older apps are untouched.
The handlers take plain requests, so the integration tests drive the
whole flow (registration, the code, tokens, refresh, the tools, another
person's content, disconnecting, expiry, pruning, account deletion)
without a web server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 12:13:00 +02:00
|
|
|
webServer:
|
|
|
|
|
port: 8082
|
|
|
|
|
publicHost: learn.halkyon.eu
|
|
|
|
|
publicPort: 443
|
|
|
|
|
publicScheme: https
|
2026-09-16 19:24:59 +02:00
|
|
|
|
2026-11-02 22:06:18 +01:00
|
|
|
# The database: the `postgres` service of deploy/compose.learn.yaml, reachable only inside
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
# the Compose network (it publishes no port), so no TLS between server and database.
|
|
|
|
|
# The password comes from the environment (SERVERPOD_DATABASE_PASSWORD, set in
|
|
|
|
|
# deploy/.env); there is no production passwords.yaml.
|
2026-09-16 19:24:59 +02:00
|
|
|
database:
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
host: postgres
|
2026-09-16 19:24:59 +02:00
|
|
|
port: 5432
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
name: halkyon_learn
|
|
|
|
|
user: halkyon_learn
|
|
|
|
|
requireSsl: false
|
2026-09-16 19:24:59 +02:00
|
|
|
#isUnixSocket: true # defaults to false
|
|
|
|
|
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
|
|
|
|
|
|
|
|
|
# This is the setup for Redis. The default for the Google Cloud Engine Terraform
|
|
|
|
|
# configuration is to connect on a private IP address.
|
|
|
|
|
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
|
|
|
|
|
# connect on the public IP of the database e.g. redis.examplepod.com.
|
|
|
|
|
redis:
|
|
|
|
|
enabled: false
|
|
|
|
|
host: redis.private-production.examplepod.com
|
|
|
|
|
port: 6379
|
|
|
|
|
#user: # defaults to empty
|
|
|
|
|
#requireSsl: true # defaults to false
|
|
|
|
|
|
|
|
|
|
maxRequestSize: 524288 # The maximum size of requests allowed in bytes
|
|
|
|
|
|
|
|
|
|
sessionLogs:
|
2026-09-24 15:02:52 +02:00
|
|
|
consoleEnabled: true
|
|
|
|
|
persistentEnabled: false
|
2026-09-16 19:24:59 +02:00
|
|
|
# consoleLogFormat: json # Defaults to "json", options are "text" or "json"
|
|
|
|
|
|
|
|
|
|
# futureCallExecutionEnabled: true # Defaults to true
|
|
|
|
|
|
|
|
|
|
#futureCall:
|
|
|
|
|
# concurrencyLimit: 1 # Defaults to 1, a negative or null value removes the limit
|
|
|
|
|
# scanInterval: 5000 # Unit in milliseconds, defaults to 5000
|
|
|
|
|
|
|
|
|
|
# Recommended when serving web clients: keeps auth tokens in httpOnly
|
|
|
|
|
# cookies. Without this, web tokens live in JavaScript-readable storage,
|
|
|
|
|
# where any XSS can steal them. Web clients opt in with
|
|
|
|
|
# `client.cookieAuth = true`. Requires `allowedOrigins`; browsers from
|
|
|
|
|
# origins not in the list lose cross-origin access, including to public
|
|
|
|
|
# endpoints.
|
|
|
|
|
#authCookie:
|
|
|
|
|
# sameSite: lax # Defaults to "lax", options are "strict" or "none"
|
|
|
|
|
# #name: serverpod_auth # Defaults to "serverpod_auth"
|
|
|
|
|
# #refreshName: serverpod_auth_refresh # Defaults to "<name>_refresh"
|
|
|
|
|
# #domain: examplepod.com # Defaults to host-only; set to share across subdomains
|
|
|
|
|
# #path: / # Defaults to "/"
|
|
|
|
|
# #secure: true # Defaults to true; the cookie is only sent over https
|
|
|
|
|
#allowedOrigins:
|
|
|
|
|
# - https://app.examplepod.com # The origin serving your Flutter web app
|