GAM runs as a Docker Compose stack behind the same reverse proxy as Gecko Notes, on
gam.geckopico.com. It shares an identity with Notes rather than having its own.
Gecko Notes must be running the suite SSO changes. They are on main as of
95ed2ca — the session cookie,
GET /api/auth/session, and the CORS/CSP headroom. GAM verifies the cookie Notes sets;
without those changes there is no cookie to verify and nobody can sign in.
The full specification, including the reasoning, is in
gecko-notes-integration.md.
JWT_SECRET_KEY=<the same value gecko-notes uses>This is not a recommendation. GAM issues no tokens — it verifies the ones Notes signs, and a shared HS256 secret is the whole mechanism. A different value rejects every session, and the symptom is an unhelpful "sign in" loop rather than a clear error.
The same secret also derives the key encrypting stored provider credentials, so changing it invalidates every session and every saved API key.
In Notes' .env:
# Sends the session cookie to every subdomain rather than only notes.geckopico.com.
AUTH_COOKIE_DOMAIN=.geckopico.comRestart Notes. Its own frontend is unaffected — it authenticates by header, and the cookie is additional.
CORS_ORIGIN on Notes only needs https://gam.geckopico.com if GAM's browser code
calls Notes' API directly. It does not today: GAM reads the shared cookie on its own
origin. Add it if that changes.
git clone https://github.com/davior/gam.git
cd gam
cp .env.example .envEdit .env:
APP_PORT=18082 # 18081 is gecko-notes
JWT_SECRET_KEY=<same as Notes>
NOTES_BASE_URL=https://notes.geckopico.com
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.ymlCOMPOSE_FILE is what joins the containers to the reverse proxy's external web
network. Without it they sit on gam_default, unreachable from the proxy, and every
request is a 502. Setting it in .env means a plain docker compose up -d cannot
forget.
docker compose up --build -d
docker compose ps
curl -f http://localhost:18082/api/health{"status":"ok","version":"…","ffmpeg":true} is the expected reply. ffmpeg: false
means the image is wrong — probing, thumbnails and transcription all shell out to it.
Point gam.geckopico.com at the frontend container on port 80, the same way
notes.geckopico.com is routed. Nothing in this repository needs to change; the
container listens on plain HTTP and the proxy terminates TLS.
The backend is exposed, never ports:-published. It trusts X-Forwarded-* from any
peer, which is correct behind a controlled proxy and a spoofing vector if the port is
reachable directly.
- Sign in at
notes.geckopico.com. - Open
gam.geckopico.comin the same browser. It should load the library with no second login. - Sign out of Notes, reload GAM: it should offer "Sign in with Gecko Notes".
- Click it, sign in, and land back on GAM — the
?redirect=parameter carries the return address, validated by Notes against*.geckopico.com.
If step 2 shows a sign-in prompt while Notes is signed in, the cookie is not reaching
GAM. Check that AUTH_COOKIE_DOMAIN is .geckopico.com (with the leading dot) and that
the browser shows a gecko_session cookie scoped to the parent domain.
Inert until configured. Fill in the RESTIC_* and BACKUP_SFTP_* settings in .env
and follow ops/backup/README.md, which documents the two
Synology DSM gotchas that cost an afternoon the first time.
Two things are backed up: ./data/db/gam.db and ./data/media/. The database is
snapshotted with sqlite3 .backup so a WAL-mode file is captured consistently; media
filenames are write-once UUIDs, which is what lets restic send only what is new.
GAM and Notes can share one Synology and one restic repository — the distinct
--host tag and BACKUP_SFTP_REMOTE_PATH keep their snapshots apart, so
restic forget prunes each app's history separately.
Nothing to configure at deploy time. Each user adds their own Deepgram key at
Settings → Speech to text; it is encrypted with a key derived from
JWT_SECRET_KEY before it is stored, and is never returned to the browser.
Also nothing at deploy time, and also per user: Settings → Semantic search. Two providers, and the choice is a privacy decision as much as a cost one.
- OpenAI needs an API key, stored the same encrypted way as the Deepgram one. Text — transcripts included — is sent to OpenAI to be embedded.
- Ollama needs no key, only an address (default
http://localhost:11434) and the model pulled on that host. Nothing leaves the machine.
Until a provider is set, search still works but matches words only, and says so. Newly
transcribed assets are embedded automatically; anything already in the library needs
POST /api/assets/{id}/embed, since embedding a whole library at once is bulk
enrichment and belongs with the rest of it in M6.
Changing the model later does not invalidate what is stored — vectors record the model that produced them and only matching ones are searched — so a switch quietly shrinks the searchable set until the affected assets are embedded again.
Nothing to configure unless YouTube refuses the server. Many VPS and datacenter IP ranges
get "Sign in to confirm you're not a bot", and age-restricted videos always need a
signed-in session. For either, export a cookies file from a Firefox profile made for the
purpose, put it at secrets/youtube-cookies.txt, and add docker-compose.cookies.yml to
COMPOSE_FILE:
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml:docker-compose.cookies.ymlThe full steps — the Firefox export, keeping only YouTube's cookies, and why a spare
account — are in url-import.md → YouTube cookies.
Not docker-compose.override.yml: with COMPOSE_FILE set, Compose never reads it.
When imports start failing across the board, YouTube has changed under yt-dlp: bump its
pin in backend/requirements.txt and rebuild.
git pull origin main
docker compose up --build -ddata/db/ and data/media/ are bind mounts, so they survive rebuilds. Migrations run
in the backend's entrypoint before uvicorn binds a port — a failed migration stops the
container rather than leaving a running app serving a schema it does not match.