This is a fork of klchdev/curio. It adds two providers that need no API key: Claude Code (CLI) and Codex (CLI). Pick one in settings and requests run through the CLI installed on the machine that hosts the server, billed to its subscription (Claude Pro/Max or ChatGPT Plus/Pro) instead of a metered key. This only makes sense when you self-host on your own computer — the server spawns
claude/codexlocally, so the binary must be on its PATH and logged in (claudeonce, orcodex login). Inside Docker there is no CLI unless you bake one into the image. The three original key-based providers still work unchanged.
A journal of what you've played that turns into advice on what to play next.
Try it without an account → · a hosted instance, free, on a made-up player's library — the same one recorded above.
Curio connects to your Steam library and asks you to write not one review after you finish a game, but several along the way — whenever your opinion moves. Write after the first evening, or forty hours in, or the moment a game loses you; there are no checkpoints to hit. Every entry is automatically stamped with your real playtime from Steam, so you can see how your opinion changed, not just how it ended up.
Those entries add up to a taste profile — what irritates you game after game, what hooks you, where your score disagrees with the way you actually talk about a game. The advice is built on that profile: your unplayed games get sorted into tiers with links back to your own words, and any single game can be put through a deep dive over its Steam reviews.
- You bring your own model key. Curio doesn't pay for inference — the key is yours, and requests go straight to the provider you picked.
- You fill the journal. Import the reviews from your Steam profile with one button, write about what you've played by hand, or spin the roulette — it picks a random game from the backlog and opens a slot for it.
- You get the write-ups. A taste profile, a tier list of what's unplayed, verdicts on individual games, and a chronicle of what you actually played and when.
A review in Curio is a thread, not a snapshot:
First impression · 45 min
"The combat is interesting, the art is nice..."
Update · 4 h 20 min
"The story is starting to sag, but the side quests save it..."
Verdict · 12 h · Finished · 4/5
"Glad I stuck with it, the ending is strong..."
Closing a game off: the rating, the verdict and what it left you with. The playtime is not typed in — it comes from Steam, which is what makes an entry comparable to the one written forty hours earlier.
Astro 6 (SSR, node adapter) · React 19 · Tailwind CSS 4 · PostgreSQL with Drizzle ORM · Steam Web API · Steam OpenID login.
You need Node.js 22.12 or newer and a live PostgreSQL database.
npm install
cp .env.example .env # fill in the variables from the table below
npm run db:push # push the schema to the database
npm run devThe app comes up on http://localhost:4321. Login goes through Steam OpenID, so
STEAM_API_KEY is needed from the very first minute — without it there's no way
to sign in.
| Variable | Req. | What it is |
|---|---|---|
STEAM_API_KEY |
yes | Steam Web API key, get one at https://steamcommunity.com/dev/apikey |
SESSION_SECRET |
yes | random string, at least 32 characters; the session cookie is signed with it |
DATABASE_URL |
yes | PostgreSQL connection string |
ENCRYPTION_KEY |
yes | any random string, at least 24 characters — encrypts users' model keys. Never change it |
CRON_SECRET |
no | password for /api/cron/poll; without it the playtime tracker doesn't run |
DEV_ACCOUNTS_ENABLED |
no | enables test accounts, off by default |
DEV_STEAM_IDS |
no | comma-separated SteamIDs allowed to use test accounts |
ENCRYPTION_KEY is the one value in the project you can neither lose nor
change. The AES key is derived from it with HKDF, so any sufficiently long
random string will do — openssl rand -base64 32 is a convenient way to get
one, not a requirement. But the derivation is deterministic: a different string
derives a different key, and every model key already in the database stops
decrypting. Save it somewhere outside the server and carry the same value across
every redeploy, or everyone using your copy has to enter their key again.
One thing is on you no matter which route you take: the Steam key. Valve
issues those to people, not to scripts — go to
https://steamcommunity.com/dev/apikey, put in any domain you like, and copy the
key out. Everything else the platform can do for you: SESSION_SECRET,
ENCRYPTION_KEY and CRON_SECRET are just long random strings, and the
database hands over its own address.
There are no migration files in this repository on purpose — the schema is
applied with drizzle-kit push, which compares the live database against
src/db/schema.ts and adds what's missing. Every route below runs it before the
server starts, so a fresh database doesn't stay empty. Re-running it costs
nothing; if a future schema change would destroy data, push refuses instead and
the app doesn't start until you deal with it by hand.
The button reads render.yaml and creates the web service and a
PostgreSQL next to it. Two clicks, not one: the first opens the blueprint, then
Render asks for STEAM_API_KEY and you press Apply.
Once it's up, copy ENCRYPTION_KEY out of the service's environment page and
keep it somewhere else. Render generated it and Render will happily generate a
different one if the service is ever recreated — and a different one can't
decrypt the model keys already in your database.
The free plan is a fair place to look around and a bad place to keep a journal: the database is deleted after 30 days, and a free web service falls asleep when nobody visits — and a sleeping process polls nothing, so the playtime tracker stops recording. Both go away on a paid plan.
Create a project from your fork at railway.com/new,
then add a PostgreSQL to it from the same canvas. Railway picks up
railway.json, builds the Dockerfile and starts the server —
no guessing at what the project is. Set the variables on the service:
| Variable | Value |
|---|---|
DATABASE_URL |
${{Postgres.DATABASE_URL}} — a reference to the database you just added |
STEAM_API_KEY |
yours from Valve |
SESSION_SECRET |
any long random string |
ENCRYPTION_KEY |
the same, and never change it afterwards |
CRON_SECRET |
the same; without it the tracker doesn't run |
Keep it at one replica — see the tracker section for why.
On any machine with Docker, this is the whole thing — the app, a PostgreSQL, a volume for it, and a one-shot job that puts the schema in place before the app is let anywhere near it:
cp .env.example .env # fill in STEAM_API_KEY and make up the secrets
docker compose up -dhttp://localhost:4321. DATABASE_URL is set inside
docker-compose.yml and ignored from .env — the
database is the db container, and a local development .env pointing at
localhost shouldn't be able to quietly break it.
To generate the secrets:
openssl rand -base64 32Logs are docker compose logs -f app; if the app never comes up, look at
docker compose logs schema first — that's where a database that refused the
schema will say so.
The Dockerfile is a plain multi-stage build with nothing
platform-specific in it, so it works anywhere a container does. It needs
HOST=0.0.0.0 (already set), a PORT, and the variables from the table above.
The schema is applied by the image too:
docker run --rm -e DATABASE_URL=... your-image npm run db:pushThe write-ups, the advice and the verdicts come from a language model, and everyone pays for their own. Curio keeps no shared key: you enter yours in the settings, it gets encrypted (AES-256-GCM), and that encrypted form is the only one the database ever holds.
Three providers are supported:
- Google Gemini — https://aistudio.google.com/apikey
- Anthropic Claude — https://console.anthropic.com/settings/keys
- OpenAI — https://platform.openai.com/api-keys
Gemini has a free tier, and it's enough to try everything except running a large journal through analysis in one go. Do keep in mind that on the free tier Google reserves the right to use the contents of your requests to improve its models; the details are in PRIVACY.md.
Steam doesn't hand out history by day and hour: playtime_forever is a single
cumulative counter, and nothing that happened between two syncs can be recovered
from it. Curio builds that history itself, off two signals:
| Signal | Source | How often | What it gives |
|---|---|---|---|
| Minute counter | GetRecentlyPlayedGames |
30 min | playtime_snapshots — how much was played, down to the minute |
| "Playing right now" | GetPlayerSummaries |
3 min | play_sessions — exactly when, down to the clock |
On their own they're useless: the counter updates in jumps (often only when you
quit the game), and the status knows nothing about minutes. Together they add up
to "Tuesday, 21:04 to 23:40, two hours forty". The /chrono page turns that into
an hourly heatmap, days, top games and recent sessions.
Both polls run on timers inside the web process and are woken when the container
starts (scripts/start.mjs pings /api/cron/poll). What production needs:
- the
CRON_SECRETvariable — without it the tracker doesn't start; - don't let the service sleep: a sleeping process polls nothing;
- a single replica. Several won't double-count (the increment is computed under a row lock, in the same transaction that writes the new value), but the number of requests to Valve grows with every one of them.
The same endpoint can be called from outside if you'd rather not keep the process alive:
curl -H "authorization: Bearer $CRON_SECRET" "https://<app>/api/cron/poll?job=all"What the tracker can't do: see a player with a private profile or in invisible mode — for them only the counter readings survive, with no session boundaries.
What is stored, where it goes and how to delete all of it — in PRIVACY.md.
One thing worth naming here: the public pages (the landing page and /try)
carry a Yandex.Metrika counter, and everything behind the login carries no
third-party script at all. The counter id comes from YANDEX_METRIKA_ID, which
is empty by default — a copy you run yourself counts nothing until you put your
own counter there. Visitors can switch it off for their browser from the
landing footer or the settings page.
Bugs and ideas go to issues, patches to pull requests. Details in CONTRIBUTING.md.
MIT — see LICENSE.
Curio is not a business. If it turned out to be useful, there's GitHub Sponsors and Boosty — two doors to the same thing, because cards from some countries are turned away by one or the other. It covers the hosting and the domain, and that's all it does. Nothing is locked behind it: there is no paid tier, no sponsor-only feature, and the hosted instance works the same whether you give anything or not. The code is MIT, so the other way to support it is a fork or a patch.
Not affiliated with Valve Corporation. Steam and the Steam logo are trademarks of Valve Corporation.

