firm: the Rails Solid stack, in pure Pythonfirm: de Rails Solid-stack, in pure Python
view on githubbekijk op github
Rails 8 shipped the Solid stack — solid_queue, solid_cache, solid_cable —
on a simple bet: most apps don’t need Redis. Your SQL database can hold the job
queue, the cache, and the pub/sub channel just fine, and running one fewer piece of
infrastructure is worth a lot.
firm is that bet in Python. It’s a pure-Python port of the Solid stack, plus one module of its own.
One package, four independent modules
Install only the part you need:
| Module | Install | Ports | Highlights |
|---|---|---|---|
| queue | pip install "firm[queue]" |
solid_queue |
concurrency controls, recurring tasks, retries, forked/threaded supervisor, crash recovery |
| cache | pip install "firm[cache]" |
solid_cache |
FIFO age/size/count eviction, pluggable coders, at-rest encryption |
| channel | pip install "firm[channel]" |
solid_cable |
broadcast/subscribe over your database, polling listener, automatic message trimming |
| audit | pip install "firm[audit]" |
(original to firm) | append-only audit log, opt-in retention, history() querying |
All four run on SQLite, PostgreSQL, and MySQL/MariaDB — verified live against all three. The top-level package imports nothing heavy, so a queue-only worker process never loads the cache, pub/sub, or audit code.
What using it looks like
Background jobs:
import firm.queue as bq
bq.configure(database_url="postgresql://localhost/myapp")
@bq.job()
def greet(name): print(f"hi {name}")
greet.enqueue("Ada") # then: firm-queue start --import myapp.jobs
Caching:
from firm.cache import Cache
cache = Cache(database_url="postgresql://localhost/myapp")
cache.fetch("k", lambda: expensive())
Pub/sub:
from firm.channel import Channel
ps = Channel(database_url="postgresql://localhost/myapp")
ps.subscribe("room:42", lambda payload: print(payload))
ps.broadcast("room:42", b'{"msg": "hi"}')
Audit log:
from firm.audit import AuditLog
audit = AuditLog(database_url="postgresql://localhost/myapp")
audit.record("invoice.paid", subject=invoice, actor=user, data={"amount": 4200})
Why “no Redis” is the point
If you already run PostgreSQL, adding Redis means another process to deploy, monitor, back up, and reason about failure modes for. For a lot of apps that cost buys very little. firm keeps jobs, cache, channels, and the audit trail in the same transactional database as the rest of your data — which also means a job and the row it writes can commit or roll back together.
How it’s built
firm is a uv workspace of independent packages — firm-core, firm-queue,
firm-cache, firm-channel, firm-audit, firm-ui — with a thin top-level firm
package that wires them together via extras. You add database drivers and features
as extras too: firm[queue,postgres], firm[cache,encryption], and so on.
The docs are built with Zensical (the Material for MkDocs
team’s successor to MkDocs), and there’s an llms.txt / llms-full.txt pair for
feeding the whole thing to an agent. Tests run on SQLite by default and against live
Postgres/MySQL when you point them at real databases.
Inspired by the Rails Solid stack from 37signals (solid_queue, solid_cache, solid_cable).
Rails 8 leverde de Solid-stack — solid_queue, solid_cache, solid_cable —
op basis van een simpele weddenschap: de meeste apps hebben geen Redis nodig. Je
SQL-database kan de taakwachtrij, de cache en het pub/sub-kanaal prima aan, en één
stuk infrastructuur minder draaien is veel waard.
firm is die weddenschap in Python. Het is een pure-Python port van de Solid-stack, plus één eigen module.
Eén package, vier onafhankelijke modules
Installeer alleen wat je nodig hebt:
| Module | Installatie | Port van | Kenmerken |
|---|---|---|---|
| queue | pip install "firm[queue]" |
solid_queue |
concurrency-controls, terugkerende taken, retries, forked/threaded supervisor, crash-recovery |
| cache | pip install "firm[cache]" |
solid_cache |
FIFO-eviction op leeftijd/grootte/aantal, plugbare coders, encryptie at-rest |
| channel | pip install "firm[channel]" |
solid_cable |
broadcast/subscribe over je database, polling-listener, automatisch opschonen van berichten |
| audit | pip install "firm[audit]" |
(origineel in firm) | append-only auditlog, opt-in retentie, history()-queries |
Alle vier draaien op SQLite, PostgreSQL en MySQL/MariaDB — live getest tegen alle drie. Het top-level package importeert niks zwaars, dus een queue-only workerproces laadt nooit de cache-, pub/sub- of auditcode.
Hoe het gebruik eruitziet
Achtergrondtaken:
import firm.queue as bq
bq.configure(database_url="postgresql://localhost/myapp")
@bq.job()
def greet(name): print(f"hi {name}")
greet.enqueue("Ada") # then: firm-queue start --import myapp.jobs
Caching:
from firm.cache import Cache
cache = Cache(database_url="postgresql://localhost/myapp")
cache.fetch("k", lambda: expensive())
Pub/sub:
from firm.channel import Channel
ps = Channel(database_url="postgresql://localhost/myapp")
ps.subscribe("room:42", lambda payload: print(payload))
ps.broadcast("room:42", b'{"msg": "hi"}')
Auditlog:
from firm.audit import AuditLog
audit = AuditLog(database_url="postgresql://localhost/myapp")
audit.record("invoice.paid", subject=invoice, actor=user, data={"amount": 4200})
Waarom “geen Redis” het punt is
Als je al PostgreSQL draait, betekent Redis erbij nog een proces om te deployen, te monitoren, te back-uppen en waarvan je de faalscenario’s moet doorgronden. Voor veel apps levert die kost weinig op. firm houdt taken, cache, channels en het auditspoor in dezelfde transactionele database als de rest van je data — wat ook betekent dat een taak en de rij die hij wegschrijft samen kunnen committen of terugrollen.
Hoe het gebouwd is
firm is een uv-workspace van onafhankelijke packages — firm-core, firm-queue,
firm-cache, firm-channel, firm-audit, firm-ui — met een dun top-level
firm-package dat ze via extras aan elkaar knoopt. Databasedrivers en features voeg
je ook als extras toe: firm[queue,postgres], firm[cache,encryption], enzovoort.
De docs zijn gebouwd met Zensical (de opvolger van MkDocs
van het Material for MkDocs-team), en er is een llms.txt / llms-full.txt-paar om
het geheel aan een agent te voeren. Tests draaien standaard op SQLite en tegen live
Postgres/MySQL als je ze op echte databases richt.
Geïnspireerd door de Rails Solid-stack van 37signals (solid_queue, solid_cache, solid_cable).