Hello world.
I love automating things. And if you do too, you probably know how this goes: a Telegram bot here, a price watcher there, a scraper on a Raspberry Pi that sits on a shelf blinking its LED, a handful of cron jobs on a cheap VPS. Dozens of small things quietly doing useful work in the background while you get on with your day.
Here's the thing though - almost every one of them eventually wants tweaking while it's running. A threshold. A toggle. A retry count. Some tiny number you should be able to change in a second, ideally from your phone, on the sofa, whilst watching that intense episode of [insert your favourite TV show here], without opening a terminal.
How it actually goes: you edit a hardcoded value or a .env, push, redeploy, wait for the pipeline to succeed. For a one-character change.
I did that. A lot. And then I built a little control panel for that price watcher. A few inputs, a save button, an endpoint the script could read from.
Then I built a second one for another project. Then a third. Somewhere around the third hand-rolled dashboard doing the same job slightly differently, I admitted this was a pattern and built the thing I actually wanted: one place to manage config for all of my running things.
That's confish.
Shape it
You define a typed schema once per application - whatever fields your thing needs, each with a type (string, number, boolean, date, array). Then you spin up as many environments as you like: staging, prod, one per client, one per Raspberry Pi. Same fields, different values. Your code hits one endpoint and gets typed config back.
Here's a script reading its config:
from confish import Confish
client = Confish(env_id="a1b2c3d4e5f6", api_key="confish_sk_...")
config = client.fetch()
print(config)
And what comes back is already cast to the types you defined - numbers as numbers, booleans as booleans, and so on:
{
"site_name": "My Application",
"max_upload_mb": 25,
"maintenance_mode": false,
"allowed_origins": ["https://example.com", "https://app.example.com"]
}
Change max_upload_mb in the dashboard and the next fetch sees 50. No redeploy, no rebuild. Suddenly your scraper's politeness delay, your bot's cooldown, your job's batch size - they're all just values you can nudge from anywhere, including that sofa.
Grow it
The schema-and-fetch core was the plan. Everything else got added because my own scripts kept asking for it.
Logs. My scripts were already talking to confish, so I wanted them to tell me how they were doing without me SSHing in to tail a file. Now the 3am cron job can simply report in:
client.logger.info("Scrape finished", {"pages": 14, "new_items": 3})
Entries land in the dashboard, per environment, with levels and context - so when something looks off, the first clue is one tab away instead of one SSH session away.
Webhooks. When config changes, confish can POST to a URL you set per environment, so a service can react instead of polling. The payload includes a changes array of exactly which fields moved, deliveries retry with exponential backoff on 5xx, and every SDK ships a one-liner signature verifier.
Command it
I saved my favourite part for last.
Sometimes I don't want to change a value - I want a running thing to do something. Flush a cache. Rebuild an index. Restart a worker. The usual answer is to expose an endpoint on the app, secure it, open it to the internet, and then quietly worry about it forever. For a personal scraper sitting behind NAT on a home server, that's a lot of ceremony - port forwarding, a reverse proxy, maybe a tunnel, maybe that "temporary" ngrok URL you forget about - all so you can say "please flush your cache."
Actions flip the direction. Your app never listens for anything. It polls. You dispatch a command from the dashboard, it sits in a queue on the environment, and your app picks it up on its next poll - acknowledges it, runs your handler, reports progress, and marks it done or failed. Nothing inbound. No open port, no public URL, no ingress rules. Works the same on a VPS, a Pi, or your old laptop in a drawer.
The raw flow is just a handful of REST calls (poll, ack, update, complete/fail), but each SDK wraps it in a consume() loop with adaptive backoff when idle:
def handler(action, ctx):
if action.type == "flush-cache":
ctx.update("Clearing cache", {"keys": 1402})
return {"cleared": 1402, "took_ms": 84}
client.actions.consume(handler=handler)
So from my phone I can hit "flush-cache" on a box that has no inbound access at all, and watch the result come back in the timeline a second later.
Wire it
There are official SDKs for Go, JavaScript, Python, PHP, and Rust (github.com/confishhq) - each wraps the same REST API, so if your language isn't on the list, plain HTTP works fine.
Try it
If you've got bots, scrapers, or cron jobs that deserve better than a redeploy per tweak, give it a spin at confi.sh - I'd genuinely love to hear what you end up wiring it to. And if you've solved this differently, or you look at the Actions model and think "hmm, not convinced" - tell me in the comments. Half the features above exist because someone (usually me) complained about something, and my scripts and bots haven't stopped asking for more - so watch this space.
Thanks for reading.
P.S. I just shipped dark mode - so when that 3am cron job has something to say, checking on it won't flashbang you.
Top comments (0)