halkyon/deploy/.env.example

184 lines
11 KiB
Shell
Raw Normal View History

# Copy to deploy/.env, restrict it (chmod 600), and fill in. deploy/.env is git-ignored
# and never goes into an image. How it reaches the server is decided when the server is
# set up (e.g. rendered from Proton Pass on the deploying machine and copied over SSH,
# so the server itself never holds Proton credentials).
# `http://localhost` for the local rehearsal; `learn.halkyon.eu` on its server.
DOMAIN=http://localhost
feat(profile): profile pages on the account domain, and links verified by rel="me" A profile now has a page on the web, at the root of the account domain (https://halkyon.eu/@raisondetredev where the operator set ACCOUNT_DOMAIN, else on the instance's web server), so the address belongs to the identity rather than to Learn and stays when other services share it (ADR 0011). The page shows what the app's profile sheet shows, in English or German, without JavaScript or cookies, asks search engines not to list it, marks every link rel="me" so other sites can verify links to it, and sends a visitor who asked on another host to its own address. Caddy forwards /@… to the web server, and serves nothing else on the account domain; compose now passes ACCOUNT_DOMAIN on, which it never did. A link is verified as Mastodon verifies one: its page links back to the profile's with rel="me". profile.checkLinks fetches each link's page, at most ten times an hour, over HTTPS on port 443 and to public addresses only, connecting to the address it checked and verifying the certificate for the host, with three redirects, 1 MiB and ten seconds at most, and keeps what links back on the profile (verifiedLinks, a column of a table no device syncs). A changed link loses its check, a new handle all of them. The app shows the page's address to copy, checks a link saved by itself and on Check links, and marks verified links in the form and in others' profiles; a check answered after a newer save only adds its checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 20:29:30 +02:00
# The domain people's handles are on (`raisondetredev@halkyon.eu`) and their profile
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't DOMAIN's host:
# point its DNS (A and AAAA) at this server, and Caddy serves the profile pages there,
# nothing else. Unset: DOMAIN's host. Decide before anyone follows a handle: it can
# never change once the fediverse knows it.
#ACCOUNT_DOMAIN=halkyon.eu
# The instance's other services (optional, ADR 0016), such as Halkyon Todo: Halkyon
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36) Every account signs in with SimpleLogin or a password now, so GitHub goes from the module, the shared app package, Learn and Todo, ahead of ADR 0017's GITHUB_SIGN_IN_ENDS. - halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or GitHub refusals; no `githubClientId`/`githubClientSecret`, no `GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register loses `account-with-github`. `SignInWays.gitHub`, the basics' `gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s `signInAgain` and `noAddress` go. A first password takes a code mailed to the person's own address; an account without one sets none (`wrongCredentials`). - Learn's server: `instance.info()` without `gitHubClientId`, the pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23 (v0.20.0) require `SignInWays.gitHub`: the server deploys with the release of build 24. - halkyon_app: no GitHub button or problems on the start screen, no password offered after a GitHub sign-in, and `ServerpodAuthGateway` without the browser half only GitHub used (`(client, base:)`). The account section offers a first password only to an account with an address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's ways back. - Learn's Profile lists no GitHub; the texts go (English, German). - Serverpod's `serverpod_auth_idp_github_account` stays, as its migrations make it; a row goes with its account, and ADR 0020's D36 gives operators the SQL to delete those left. - deploy/: compose.yaml and .env.example without GitHub. The READMEs, AGENTS.md files and ADR 0017 amended. BREAKING CHANGE: builds up to 23 can't read the instance's answers about an account's ways in; minimumAppBuild is 24. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
# Learn holds the instance's register of accounts, so an address, a SimpleLogin account
# and a handle are one person in every service, and deleting the account deletes it in
# all. The register learns when someone signs in to which service (the privacy page says
# so).
# INSTANCE_SECRET is the secret the services share, carried by every call between them:
# the same value in each service's environment, never given to a device. Empty: no
# service calls another. Generate: openssl rand -hex 32
#INSTANCE_SECRET=
# The web servers of the other services as this server reaches them on the instance's
# own network (never through the internet), separated by commas or spaces
# (`http://todo-server:8082`): told when an account goes, so each deletes that person's
# data too, and told again hourly until each answers. Each of them sets, in its own
# environment, the same INSTANCE_SECRET and ACCOUNT_REGISTER_URL, this server's web
# server as it reaches it (`http://server:8082`). Learn never takes ACCOUNT_REGISTER_URL:
# it doesn't start with one, as it holds the register.
#INSTANCE_MEMBERS=
feat(profile): profile pages on the account domain, and links verified by rel="me" A profile now has a page on the web, at the root of the account domain (https://halkyon.eu/@raisondetredev where the operator set ACCOUNT_DOMAIN, else on the instance's web server), so the address belongs to the identity rather than to Learn and stays when other services share it (ADR 0011). The page shows what the app's profile sheet shows, in English or German, without JavaScript or cookies, asks search engines not to list it, marks every link rel="me" so other sites can verify links to it, and sends a visitor who asked on another host to its own address. Caddy forwards /@… to the web server, and serves nothing else on the account domain; compose now passes ACCOUNT_DOMAIN on, which it never did. A link is verified as Mastodon verifies one: its page links back to the profile's with rel="me". profile.checkLinks fetches each link's page, at most ten times an hour, over HTTPS on port 443 and to public addresses only, connecting to the address it checked and verifying the certificate for the host, with three redirects, 1 MiB and ten seconds at most, and keeps what links back on the profile (verifiedLinks, a column of a table no device syncs). A changed link loses its check, a new handle all of them. The app shows the page's address to copy, checks a link saved by itself and on Check links, and marks verified links in the form and in others' profiles; a check answered after a newer save only adds its checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 20:29:30 +02:00
# The instance's assistants, which an operator grants to people (Halkyon Learn's
# ADR 0021): in a file of their own, deploy/assistants.env, which goes to the server
# alone (assistants.env.example says how). Without it, no assistant.
# Sidecars switched on beside the instance (sidecars/README.md), each by its compose file
# after the instance's own: compose.jellyfin.yaml, a Jellyfin people watch and listen to
# learn from; compose.languagetool.yaml, spelling and grammar checked; compose.train.yaml,
# Halkyon Train's server with its BRouter. Unset: the instance alone.
#COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
# to its people (the app's Jellyfin sign-in fills it in).
#JELLYFIN_DOMAIN=jellyfin.example.org
# With Halkyon Train beside the instance (compose.train.yaml, sidecars/README.md,
# Train): the domain its apps reach it at, its database's password, its own session
# pepper (never change it once people sign in), INSTANCE_SECRET above set, and the
# region of BRouter's segments, `west,south,east,north` in whole degrees, multiples of
# 5 (empty: Europe's, -25,35,40,70, about 3.3 GB). Generate each secret:
# openssl rand -hex 32. Its server is told of accounts gone without INSTANCE_MEMBERS;
# setting it, name Train's too (`http://train-server:8082/`).
#TRAIN_DOMAIN=train.example.org
#TRAIN_POSTGRES_PASSWORD=
#TRAIN_SESSION_KEY_HASH_PEPPER=
#BROUTER_REGION=
# The Loop's firmware through the instance (Train's ADR 0030, D40): `off` to stop
# relaying it; empty: on.
#LOOP_FIRMWARE_RELAY=
# Train's map (ADR 0030, D22): a MapLibre style over HTTPS, with its name, credit and
# page; empty: OpenFreeMap's.
#MAP_STYLE_URL=
#MAP_SOURCE_NAME=
#MAP_ATTRIBUTION=
#MAP_ABOUT_URL=
# With Train's own map (compose.maps.yaml, sidecars/README.md, Train's map; ADR 0030,
# D50), after compose.train.yaml in COMPOSE_FILE: the secret its tiles' keys are made
# with (openssl rand -hex 32), and the region cut, `west,south,east,north` in degrees
# (empty: Europe's, -25,35,45,75). MAP_STYLE_URL is then not used.
#MAP_KEY_SECRET=
#MAP_REGION=
# Postgres password, shared by the postgres and server services.
# Generate: openssl rand -base64 32
POSTGRES_PASSWORD=
# Pepper for hashing session keys (server-side sessions). NEVER change it once
# sessions exist: everyone would be signed out, and anonymous accounts can't sign
# back in. Rotation only via fallbackSessionKeyHashPeppers.
# Generate: openssl rand -base64 32
SESSION_KEY_HASH_PEPPER=
feat(server): GitHub sign-in that keeps only the GitHub ID As decided, a GitHub sign-in keeps nothing but the numeric GitHub ID, so the login is our own, on Serverpod's GitHub helpers (lib/src/auth/github.dart): Serverpod's stock login also stores the name and email in a profile and downloads the avatar, and its endpoint is never exposed. The server exchanges the authorization code with the client secret, reads the ID, and drops the access token. GitHubUpgradeEndpoint links the GitHub ID to the signed-in anonymous person, grants identified (to their existing sessions too) and removes the anonymous identity, in one transaction: the person, their spaces and their studysets stay the same. GitHubSignInEndpoint gives a session for a person who has the GitHub account already; it's called without a session, since Serverpod issues no session for another person inside a signed-in call, and it never creates people. InstanceEndpoint.info() tells the app the instance's GitHub client ID. Refusals are GitHubSignInRefusedException with a reason: GitHub sign-in off here, the account taken by another person, another GitHub account linked already, an unknown account. GitHub sign-in is on only where the passwords hold githubClientId and githubClientSecret (empty counts as missing, as Compose passes unset variables on as empty strings); production takes them from GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in deploy/.env. grantIdentified can now join a caller's transaction. No migration: the GitHub accounts table comes with the auth module. Tests, refusals first: upgrading when not signed in (checked by switching the guard off: the test fails), both ways without a GitHub app, an account taken by someone else with nothing changed, a second GitHub account, an unknown account with nobody created; then linking keeps the person and reaches their sessions and stores no email, linking twice changes nothing, signing in reaches the right person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 13:35:51 +02:00
# Email sign-in (optional, ADR 0010): people sign in with a code mailed to their address,
# of which the instance keeps only a keyed hash. Off unless the pepper and all of the SMTP
# service's settings are set; the app then doesn't offer it.
# The pepper: NEVER change it once anybody signs in with an address, as every address's
# hash would be lost, and with it those people's way in.
# Generate: openssl rand -hex 32
EMAIL_SIGN_IN_PEPPER=
# The SMTP service codes go through, over TLS only (STARTTLS, or TLS directly on 465).
# Proton (a paid plan with an address on your own domain): smtp.protonmail.ch, 587, the
# address as the user, an SMTP token made for it in Proton Mail's settings (IMAP/SMTP) as
# the password, and the same address as EMAIL_FROM. Some hosts block outgoing 25 and 465;
# 587 gets through.
EMAIL_SMTP_HOST=
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=
EMAIL_SMTP_PASSWORD=
# The address codes come from (shown as "Halkyon Learn"); its domain needs SPF, DKIM and
# DMARC records for the service, or mail lands in spam.
EMAIL_FROM=
# Password sign-in (ADR 0017): an address, a password that never leaves the device, and
# a code from an authenticator app. Off unless all three keys and email sign-in are set
# up (resets go by mail); each key at least 32 characters, or the server doesn't start.
# They stay outside the database, so a copy of it alone gives none of this away:
# TOTP_KEY seals the authenticators' secrets: changing it makes every authenticator
# useless (people then reset theirs, 72 hours, by mail).
# RECOVERY_CODE_KEY keys the recovery codes' hashes: changing it voids every recovery
# code (people make new ones once signed in).
# SIGN_IN_DECOY_KEY makes the answers for addresses nobody has look like an account's:
# changing it lets someone who watched those answers tell which addresses are unused.
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7) Someone whose SimpleLogin identity is new to the register but who has an account ("I already have one") names their handle and proves the account's recovery phrase; the new identity takes the old one's place. - `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the phrase's key, the public keys and a nonce, for the account with that handle (whatever its case). Only an account with keys and a SimpleLogin identity is mended, a link it had, as nobody adds SimpleLogin signed out (D10); a handle nobody has, or any other account's, gets a decoy from `signInDecoyKey`, the same each time. At most 10 an hour per handle and 30 per network. - `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`, binding the attempt (`mendLinkDigest`, on both sides), so a proof relayed from another sign-in links nobody's identity but the device's own; checked for the waiting identity first, which leaves the proof unspent. Every session of the account ends, in every service, and the sign-in stands ten minutes for a change, such as a lost password. - SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys (`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a SimpleLogin app and without it (`checkSimpleLoginMending`). - Members ask the register for both; `RemoteRegister` and the instance API carry them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:26:58 +02:00
# SimpleLogin needs it alone (below), for handles nobody has.
# Generate each: openssl rand -hex 32
TOTP_KEY=
RECOVERY_CODE_KEY=
SIGN_IN_DECOY_KEY=
# SimpleLogin (ADR 0020): people sign in with their SimpleLogin or Proton account, of
# which the instance keeps only SimpleLogin's ID for them (no address, no name). Off
# unless both are set: the client ID and secret of this instance's own SimpleLogin app
# (SimpleLogin → Settings → Developer → New website), registered with the callback
# /simplelogin-callback on the server's web address (on learn.halkyon.eu's server
# https://learn.halkyon.eu/simplelogin-callback), its forwarding page back to the apps.
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7) Someone whose SimpleLogin identity is new to the register but who has an account ("I already have one") names their handle and proves the account's recovery phrase; the new identity takes the old one's place. - `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the phrase's key, the public keys and a nonce, for the account with that handle (whatever its case). Only an account with keys and a SimpleLogin identity is mended, a link it had, as nobody adds SimpleLogin signed out (D10); a handle nobody has, or any other account's, gets a decoy from `signInDecoyKey`, the same each time. At most 10 an hour per handle and 30 per network. - `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`, binding the attempt (`mendLinkDigest`, on both sides), so a proof relayed from another sign-in links nobody's identity but the device's own; checked for the waiting identity first, which leaves the proof unspent. Every session of the account ends, in every service, and the sign-in stands ten minutes for a change, such as a lost password. - SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys (`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a SimpleLogin app and without it (`checkSimpleLoginMending`). - Members ask the register for both; `RemoteRegister` and the instance API carry them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:26:58 +02:00
# Takes SIGN_IN_DECOY_KEY (above), or the server doesn't start: a broken link is mended
# with the handle and the recovery phrase, a handle nobody has answered as an account's.
SIMPLELOGIN_CLIENT_ID=
SIMPLELOGIN_CLIENT_SECRET=
# Another callback than this server's own page, as the app registered it (https only).
#SIMPLELOGIN_CALLBACK_URL=https://learn.example.org/simplelogin-callback
# The ways in the app offers, of those set up above: simplelogin, password, or both
# (separated by spaces or commas). Empty: every way set up. An unknown name stops the
# server as it starts.
#SIGN_IN_WAYS=simplelogin
feat(base)!: sign-ups as the operator says, waiting accounts, operators (ADR 0021, step 1) SIGN_UP takes open (unset), approval or closed; anything else stops the server as it starts. Closed refuses a SimpleLogin sign-up before its handle is counted, an address sign-up only once the code has proved the address, so nothing tells whether an address has an account. Approval lets a new account wait, at most ten at once: no session is issued for a waiting or suspended account, only a pass (32 random bytes, kept as its SHA-256, 30 days, five per account) and four BIP-39 words the operator reads out to match it. The pass looks, forgets, and deletes an account that was never approved; nothing else. Operators hold the scope operator, given first by SQL (D32), then named in the app: InstanceOperatorEndpoint lists, approves and refuses waiting accounts, finds accounts, suspends them (ending their sessions), and names and takes back operators, never the last. account.operating() counts the waiting for the badge. Members ask the register (five new operations, a refusal kind signUp), and say closed while it is out of reach. Never-approved accounts go after 14 days in the daily pruning. The migration only adds the tables waiting_account and waiting_pass, so the server before it keeps working on the new schema. BREAKING CHANGE: a register and its members must run this version together: a member asks the register operations an older one doesn't know. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:04:21 +01:00
# Who may make an account (Learn's ADR 0021): open (empty), approval (new accounts wait
# until an operator approves them in the app, at most 10 at once, deleted unapproved
# after 14 days) or closed (nobody new). The first operator is made by SQL once signed
# up: docs/adr/0021-private-instance-and-assistant.md, D32. Anything else stops the
# server as it starts.
#SIGN_UP=approval
# Where the source of what this instance runs is, shown in the app's About dialog: both
# licences (AGPL for the app, SSPL for the server) require offering it to the people who
# use the instance. Empty: the project's repository, which is right for unmodified code.
# Running modified code, set it to where your modified source is published.
SOURCE_URL=
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
# The languages besides Japanese whose dictionaries this instance keeps (the dictionaries
# service): Chinese from CC-CEDICT, weekly (some 4 MB), in simplified and traditional
# characters; Korean from KRDict, about monthly (some 84 MB); their characters from
# Unihan, weekly (some 8 MB); the rest from Wiktionary (German and Greek with English
# meanings, English with German ones; for Korean the forms of its words and the
# readings of its Hanja), whose whole dump (some 3 GB) it downloads whenever kaikki.org
# makes a new one, about monthly, and converts in ten minutes. Unset: de,el,en,ko,zh.
# Empty: none. Files of a language taken out are removed, and the server unloads them.
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
#DICTIONARY_LANGUAGES=de,el,en,ko,zh
# The Jellyfin server the app suggests to this instance's people, who watch and listen
# to learn from it (Halkyon Learn's Watch screen). Empty: `https://jellyfin.` on the
# domain registered for this instance (learn.example.org → jellyfin.example.org), if a
# Jellyfin server answers there; `none`: no suggestion.
JELLYFIN_URL=
# Where this instance keeps people's data, and its backups, which the app shows as a flag
# and in words when someone chooses an instance: `EU`, a country's code (ISO 3166-1
# alpha-2, `DE`, `CH`) or a subdivision's (ISO 3166-2, `DE-BY`, `US-CA`); for the backups
# also `none`. As precise as you know: the app adds "(EU)" to a member country and the
# country to a subdivision ("Germany (EU)", "California, United States"). Where the disks
# are, not which law governs you as the operator: claim only that. Empty: the app says
# nothing.
DATA_LOCATION=
BACKUP_LOCATION=
# How much room people's images and sounds may take on this instance, in MiB. Empty: the
# defaults, 500 for a person and 15000 for everyone together (0: no limit for everyone
# together). Every file is in the database, so also in each of the 3 dumps
# this server keeps (and a fourth while a deploy writes its own): files take five times
# their size on the disk. One person may get more than the default; as the operator,
# in deploy/ (their id is their account's, "authUserId"):
# docker compose exec postgres psql -U halkyon_learn -d halkyon_learn -c "INSERT INTO
# file_allowance (\"authUserId\", \"maxBytes\", note) VALUES ('<id>', 2147483648, 'asked by mail')"
FILE_LIMIT_IDENTIFIED_MB=
FILE_LIMIT_INSTANCE_MB=