How Soli Compares
Soli is a batteries-included MVC framework shipped as a single Rust binary — language, server, ORM, template engine, test runner, formatter, linter, LSP and deploy tooling in one executable. No Node, no bundler, no dependency install between you and a running app.
An honest tour
This page covers what Soli gives you today, where it genuinely stands out against Rails, Laravel, Phoenix and Django — and what it does not have yet. We'd rather you find the gaps here than in production.
The Philosophy
- One binary, three commands.
curlthe binary,soli new,soli serve. Nobundle install, nonpm install, no compile step for your app code — a generated app has nopackage.jsonat all, and Soli compiles its Tailwind with a checksummed standalone binary it fetches once per machine. - Convention over configuration, Rails-style: auto-loaded models/controllers/policies, RESTful
resourcesrouting,*_pathhelpers, ERB-style views. - Server-rendered first. htmx + Alpine.js ship vendored in every new app; instant navigation and hover prefetch are built into the server. No JS build pipeline — by design.
- SoliDB-native by default. The full ORM surface (graph, vector, columnar, timeseries, raw SDBQL) speaks SoliDB.
SOLI_DB_ADAPTER=postgres,mysql, orsqlitegives document CRUD + aggregates + batchedincludes, andtable "…"maps a model onto a table's real columns (CRUD, hash.where, aggregates, batched associations,group_by, bulk writes, transactions,encrypts, STI), which migrations can now create portably, over a TLS connection when the server wants one — still not full SQL parity (no composite primary keys; raw SQL goes throughModel.find_by_sql), and deliberate about that ceiling. Seedocs/sql-adapter-design.md. - Fast by default. 170,000+ req/s on a single server: engine-per-worker architecture, production bytecode VM, in-memory asset cache.
What's in the Box
| Layer | What you get |
|---|---|
| Routing | Verb helpers, RESTful resources (nesting, member/collection), namespaces, :param + *splat segments, named *_path/*_url helpers, per-route and scoped middleware |
| Controllers | Class-based with inheritance, before_action/after_action (with only:/except:), per-action layouts, @ivar auto-exposure, respond_to content negotiation, halt, local-only redirect |
| Views | ERB-style templates, auto-escaped by default, layouts + partials with locals, content_for/named yield, Rails-style form builder (form_with(post) do |f| blocks — derived URLs/verbs, value prefill, validation errors, per-form CSRF tokens, button_to, _method override, fields_for nested sub-builders), Rack-style nested params (author[name], tags[]), markdown views, i18n/date/url helpers, ammonia-backed sanitize_html |
| ORM | Query builder, associations (belongs_to, has_many incl. through:, has_one, HABTM, polymorphic), single-collection inheritance (STI), dirty tracking, cascade deletes, counter caches, eager includes in one round-trip, grouped() read-coalescing, keyset-paged batch iteration (find_each/in_batches), scopes, callbacks, validations, soft delete, encrypted attributes, transactions, state machines, native graph edges with traversal/shortest-path queries, insert-only timeseries collections with time_bucket aggregation and prune retention, grouped multi-aggregate analytics (group_by/aggregate/having), columnar stores for append-and-aggregate data, and declared-index search: vector ANN (similar), fulltext (search), geo (near/within), graph-augmented + one-call RAG (graph_rag/rag) |
| Realtime | WebSocket rooms + presence tracking, SSE (handler-driven sse(req) and async pub/sub sse_broadcast), one-call broadcast() to WS + SSE together, LiveView (DOM-morphing patches, streams, live queries, debounce/throttle, JS commands, hooks, loading/disable-with, click-away, soli-patch/soli-live navigation, nested child sockets, soli-upload, reconnect restore, redirect/soli-href), live reload in dev |
| Auth & security | soli generate auth (Argon2id, session-fixation defense, password reset, email confirmation, remember-me, account lockout, per-IP throttling, timing- and message-uniform sign-in), Pundit-style policies (deny by default), attr_accessible + nested permit() mass-assignment whitelists, schema-based input validation (V, with type coercion), security headers on by default in prod, layered CSRF (Origin/Referer gate + verified per-form tokens, with the app's public hostnames declared in SOLI_APP_HOSTS rather than read from a proxy header an attacker can forge), declarative CORS (cors("/api/*", {...}) — preflights, response headers, origin-checked CSRF opt-in), signed/encrypted cookie jar (set_cookie(..., {"signed"/"encrypted": true}) + read_cookie — HMAC-SHA256 / AES-256-GCM, name-bound, embedded expiry), SSRF-hardened outbound requests (loopback and private ranges refused by default for HTTP.*, Web Push, webhook delivery and PDF image sources alike; exceptions named by host:port in SOLI_HTTP_ALLOW_HOSTS, matched on the literal URL host so a DNS answer can't widen it), a filesystem jail every path-taking builtin resolves through, and request-edge limits that are on by default rather than a deployment exercise — body size and read time, TCP and WebSocket connection counts, WebSocket message rate, parameter counts, page sizes, and a wall-clock budget per handler, so one client cannot hold a worker or exhaust the process. Encrypted app bundles |
| Jobs & mail | Job.perform_later/perform_in/perform_at/perform_now, cron DSL, webhook jobs with HMAC signing — in-process engine with atomic claiming, retries with backoff, and lease-based crash recovery on any adapter (SolidB, Postgres, MySQL, SQLite); standalone soli jobs worker + /__soli/jobs dashboard; ActionMailer-style mailer with SMTP/STARTTLS, multipart, attachments, test mode |
| Documents | pdf_render, Factur-X / EN 16931 e-invoices (PDF/A-3b), PAdES digital signatures + timestamping, tagged PDF/UA output, merge/fill/stamp, markdown→PDF, in-process PNG page previews (pdf_preview — no poppler, no PDF.js), ready-made invoice & quote templates, spreadsheets (CSV/XLSX read+write) |
| Tooling | Parallel test runner with per-worker isolated DB + coverage gate (HTML/JSON/Cobertura), formatter, linter, static type checker (soli check), LSP + editor plugins, app-aware TUI REPL (soli in an app dir loads your models + DB — a rails console), soli routes route lister, soli graph build code-graph in SolidB for agents (graph RAG over your own source — semantic search + relationship traversal, instance-call / partial / redirect / super edges on Soli apps, soli graph query --kind / --path for agents; works on any repo — Ruby/Rails, Python, JS/TS, Rust, C# via tree-sitter), opt-in OpenAPI spec + Scalar API reference (SOLI_OPENAPI), scaffold/auth/mailer generators, engines (mountable sub-apps), soli deploy, soli cloud (immutable releases behind a mutable alias — health-gated cutover, one-symlink rollback, --dry-run plan), soli env (a running environment per branch: git worktree + its own migrated database + its own subdomain), soli serve on any folder (a directory that is not a Soli app is served as files: rendered Markdown, generated folder indexes, .slv/.erb templates, extra read-only roots via --assets, live reload), self-executing app binaries (soli build --standalone, cross-target) with signed over-the-air auto-update (--update-url, soli update-keygen/sign-update) |
| Long tail | S3 client, image pipeline, web push (VAPID) and a native bridge (Native.notify) that raises OS notifications inside packaged desktop/mobile shells, where web push does not exist at all, JWT and PASETO v4 tokens, TOTP primitives, feature flags with percentage rollout, i18n, KV store (Redis-style API), SOAP client, POP3 + IMAP mail clients |
Where Soli Stands Out
Performance per server
A tree-walking dev mode and a bytecode VM in production, engine-per-worker with no GIL and no process fleet. On the matched benchmark — same payload, same 16 workers, same box — a rendered HTML page measures 125,000 req/s for Soli against 11,200 for Rails and 7,100 for Django, and a database-backed page 39,100 against 8,200 and 5,500. Once a real query is in the request the multiple compresses to 4–5×, not the 10× the trivial routes show. Phoenix is the one genuine peer, and it is now measured rather than asserted: on the database read the two are a dead heat on CPU — 356µs system-wide for Soli against 357 for Phoenix — with Soli 1.12× ahead on throughput. On writes Phoenix is simply faster: 1.3–1.6× Soli on create, update and delete, because Soli's writes leave the process as HTTP to SoliDB where Ecto uses a pooled binary connection. See Benchmarks for all eight stacks, which is candid about every one of the four cells Soli loses.
Deployment simplicity
One static binary runs your app, bundles it (soli build) into a single .soli file, or compiles app + runtime into one self-executing binary (soli build --standalone, cross-buildable for linux-amd64/linux-arm64/darwin-amd64/darwin-arm64/windows-amd64) — optionally AES-256-GCM encrypted with RAM-only extraction for licensed/on-prem distribution, and with signed over-the-air auto-update (--update-url: P-256-signed manifests, sha256-gated, atomic self-replace) so a shipped artifact stays current. Those last capabilities have no first-party equivalent in Rails, Laravel, Phoenix or Django. For server deploys, soli cloud lands an artifact in a release directory that is never modified again, health-gates it, and only then repoints the alias — so a rollback is a symlink move rather than a rebuild — and soli env up --branch feat/cart gives a branch its own subdomain, worktree and migrated database. The other four leave both to a separate tool (Kamal, Capistrano, Envoyer/Forge) or to a hosting platform. The same binary also doubles as a zero-config static & Markdown server — soli serve ./notes on any directory — where the other four have no equivalent command at all.
Container-native lifecycle
Liveness (/_health) and readiness (/_ready) endpoints, and a graceful drain on SIGTERM: readiness starts failing so the load balancer stops routing, new requests get a clean 503, and requests already in flight run to completion before the process exits (bounded by SOLI_SHUTDOWN_GRACE_SECS, default 25s). Rolling deploys don't truncate responses. Prometheus metrics at /_metrics, structured JSON logs (SOLI_LOG_FORMAT=json), and OpenTelemetry traces (W3C traceparent + OTLP/HTTP) round it out. Parity with the big four here, not an edge — but it means no sidecar or wrapper script is needed to deploy on Kubernetes or ECS.
Developer experience in --dev
The dev bar shows per-request queries with bind variables and durations, N+1 detection with a suggested fix, a zoomable flamegraph, per-template and per-middleware timings, and captures every XHR/htmx call a page makes — and can replay any captured request server-side to reproduce a bug. There are also preview galleries for view components (/__soli/components) and mailer templates (/__soli/mailers), and a mail inbox (/__soli/inbox) that catches every email the app sends — searchable, with HTML/text/raw tabs and .eml download, and no local SMTP server required (Rails needs the letter_opener gem or a separate MailCatcher process for this). The dev error page includes a live REPL evaluated against the failed request's actual state. This is beyond what any of the big four ship out of the box.
Documents and compliance
European e-invoicing (Factur-X/EN 16931) down to the French mandate's party identifiers (SIREN/SIRET, BT-30/BT-47), PAdES-compliant digital signatures with timestamp authorities, PDF/A and tagged PDF/UA output, form filling, stamping and merging, and page images for previews and thumbnails — built in, in-process. In every other framework this is a third-party integration project, and the preview half usually means shelling out to poppler or shipping a PDF.js bundle to the browser.
Frontend without a toolchain
Auto-escaped server templates, vendored htmx + Alpine, Turbo-Drive-class instant navigation (body swap, or an opt-in morph that keeps unchanged nodes) and hover prefetching with ETag revalidation — zero Node, zero config.
Shipping to a device
soli generate client android|ios|linux|windows writes a thin OS shell that loads your deployed app and speaks a native bridge: Native.notify raises a real OS notification when the app is closed, where web push does not exist at all. soli generate devices adds the token-registration endpoint, and APNs and FCM are both first-party. soli desktop build is the other shape — a local product with an embedded database rather than a shell over a deployment. Deep links, camera and microphone permissions, geolocation, motion sensors, barcode scanning and an offline story are documented per platform, including where each host refuses. None of Rails, Laravel, Phoenix or Django generates a shippable mobile or desktop client at all; this is the one edge here that is about distribution rather than the server.
Honest Gaps
Things a Rails, Laravel, Phoenix or Django developer will look for and not find. Some are on the roadmap; some are deliberate trade-offs.
Framework surface
- No auto-generated admin panel — a deliberate choice in 2026: every new app ships agent-ready (
CLAUDE.md/AGENTS.md, permission presets,/soli-resource), so the bet is that a coding agent plussoli generate scaffoldbuilds you a bespoke admin faster than you'd customize a generic one. If you want a Django-admin-style CRUD UI with zero building, it isn't here. - Attachments are first-class.
has_one_attached("avatar")/has_many_attached("photos")store on disk by default (ors3/solidb), generateattach_/detach_/_urlmethods, and purge ondelete. The olderuploader(...)form still defaults to SoliDB blobs. Ingest is buffered in memory under a per-request cap and an aggregate in-flight budget, so uploads are bounded rather than streamed: there is no direct-to-storage browser upload and no resumable/tus path. - View composition — layouts, partials with locals,
content_for/namedyield, plus a built-incomponent(name, data)helper (looks incomponents/, scaffold withsoli generate component) with a block form for default + named slots (ac.slot(...)slot-builder), collection rendering, declared props with dev/lint warnings, opt-in fragment caching, a dev component catalog at/__soli/components, andpaginate(pagination)for list navigation. Not a full ViewComponent system (no component classes), but a practical, growing step up from raw partials. - LiveView is young. Patching is DOM-aware — the client morphs nodes in place with keyed reconciliation (
soli-key/soli-ignore), so focus and widget state survive updates — and it has collection streams, reactive live queries, debounce/throttle, JS commands,soli-patch/soli-livenavigation, nested child sockets, shared-statelive_componentassigns,send_update/ childevent == "update", client hooks, loading/disable-with, click-away, chunkedsoli-upload, and reconnect state restore. Child components are not OTP processes (no cross-processsend_update). Pause/resume of a mid-flight upload is still missing. - EUI is the second real-time UI stack, and it is not a browser one. A component declared with
router_euiuses the same LiveView machinery — same registration, same{event, params, state} -> statehandler, same worker pool — but its view returns a node tree as plain data rather than an HTML template, and the server sends it as compact binary patches over/_eui/session/<component>. A Rust client lays it out and draws it on the GPU, so there is no document engine running and a counter idles at a few megabytes and no CPU. It is in the default feature set. The cost is the client: this reaches a native application, not a web page — a browser opening an EUI address gets a short page explaining what the address is. A page can also be served as one render with no session behind it, and one corner of it made live with an island, which is how a mostly-static page stops paying a session per reader; the client half of islands is not built yet.
Auth
The primitives are all here, with no third-party packages: session auth (soli generate auth, Argon2id), JWT API auth (jwt_sign/jwt_verify) and PASETO v4 for teams who would rather not have a negotiable algorithm at all, native TOTP 2FA (Crypto.totp_generate/verify/uri), and OAuth client scaffolding via soli generate oauth github|google (state CSRF, find-or-create user, session login) — plus guides for GitHub, Google, and TOTP.
Soli can also be the identity provider. soli generate oidc_provider scaffolds a working OpenID Connect provider — Authorization Code with PKCE, discovery and JWKS, consent screen, refresh-token rotation with reuse detection, and key rotation via kid. Rails, Laravel and Django all reach for a third-party package here (Doorkeeper, Passport, django-oauth-toolkit). Be equally clear about the ceiling: it implements the code+PKCE profile and nothing else — no dynamic client registration (RFC 7591), no request objects (JAR), no implicit or hybrid flows, no client-credentials or device-code grants, no front/back-channel logout, no DPoP or mTLS sender constraining, and no token introspection (RFC 7662). Native apps needing custom-scheme redirects are not supported yet either.
OAuth client (sign in with a provider) is now a generator too:
soli generate oauth github / google
(requires auth first). See
OAuth Client.
The generated flow uses state CSRF and PKCE S256, and status-checks every provider response.
Ceiling: GitHub and Google only in v1 — not a full OmniAuth catalog.
soli generate auth is a one-command, Devise-style suite: login/signup plus password reset (hashed one-time tokens, 2h expiry, no account enumeration), email confirmation with resend, remember-me (HttpOnly + Secure persistent cookie, digest-stored token, revoked on password reset), per-IP throttling on all three credential endpoints, a 12-character minimum enforced from one place by both sign-up and reset, and account lockout with auto-unlock — all as readable scaffolded code you own, with the thresholds as constants at the top of the generated User model. Sign-in is enumeration-resistant on both channels: same message and same Argon2id cost whether or not the address exists, with the “locked” message held back until the password verifies. What's still assembled by hand: unlock emails and a change-email flow.
Data layer
- SoliDB is the full stack; the SQL adapters cover two narrower modes. Set
SOLI_DB_ADAPTER=postgres,mysql, orsqlite+DATABASE_URLfor document CRUD, hash-style.where(equality, comparisons,IN,LIKE,OR), sum/avg/min/max, multi-rowgroup_by, batchedincludes(belongs_to/has_many/has_one/HABTM/through:, including a hash filter on the related rows),.join,.having, transactions, SQL migrations,soli db:schema:dump/load, andsoli db:import. Declaringtable "orders"instead maps a model onto a schema's real columns — introspected at boot, with the same portable query surface (CRUD, hash filters, aggregates,.join/.having, batched associations including HABTM andthrough:, bulk writes, atomic counters,soft_deletewhen the table has adeleted_atcolumn,encryptson text columns, and STI when the table has a stringtypecolumn); composite primary keys are not supported there. Migrations build those tables portably (create_table("orders", { … })with a column hash, plusadd_column/add_index), and a migration can declare the connection it belongs to.Model.find_by_sqlis the raw-SQL escape hatch. Indexes declared withindexare created as expression indexes, and the dev bar's query log, N+1 detection and--fail-on-n1cover SQL. Connections to Postgres and MySQL are TLS-capable (rustls):?sslmode=/?ssl-mode=with the libpq ladder fromdisabletoverify-fulland an optional CA file, opportunistically encrypted by default — so a managed database (RDS, Cloud SQL, Neon, PlanetScale) no longer needs a proxy in front of it. Graph, vector, columnar, timeseries, and raw SDBQL remain SoliDB-only. SQLite runs the same surface from a single file with no server — one writer at a time, and no exact numeric type. See Column-aware models, PostgreSQL, MySQL, and SQLite. - Dirty tracking is value-based (
changed?,changes,previous_changes,attribute_wasexist, but in-place mutation of a nested Hash/Array isn't tracked — noattribute_will_change!), and cascade deletes / counter caches skip bulk writes (delete_all/update_all— matching Rails;reset_countersrepairs drift).through:eager loading works on the SQL adapters (document and column mode); the AQL shape on SoliDB still declines it. - Migrations roll back one step at a time — no
STEP=n, no version targeting, nodb:reset.soli db:schema:dump/db:schema:loadcapture and restore dialect SQL plus applied versions on the SQL adapters. - Uniqueness validation is only race-safe when backed by a database unique index — the same caveat as Rails'
validates_uniqueness_of; Soli maps the index's 409 back into_errorsautomatically. (Conditionalif:/unless:and per-operationon: create/updatevalidation forms exist.)
Testing
The runner (parallel workers, isolated per-worker databases, coverage with a CI gate) is strong. with_transaction rolls back per-example DB writes; freeze_time/travel_to pin datetime_now(); factories support callable templates, #{n} interpolation, and Factory.insert. Request specs can also assert on the database itself — assert_no_n_plus_one, assert_query_count, assert_max_queries, or the suite-wide soli test --fail-on-n1 guard — failing on N+1s and query-budget regressions that Rails and Laravel only surface through third-party gems (Bullet, prosopite). Browser testing is built in: soli test --browser drives a real headless Chrome over the DevTools protocol — visit, click, fill_in, assert_text — with no Node, no npm and no Playwright install, and the HTTP cookie jar carries into the browser so login() still works. Responsive behaviour is testable too: a viewport("mobile") declaration in a describe body renders every test in it at an emulated device, touch and pixel ratio included — the equivalent of Capybara needing a driver-specific resize_window. Still missing: no mocking/stubbing framework, and no test filtering/watch/fail-fast flags.
Operations
- No TLS in-process — you front Soli with a reverse proxy (HTTP/2 is cleartext-only behind it). Deliberate, but different from Phoenix.
- Jobs can scale off HTTP —
soli jobs(aliassoli worker) runs the poller and pool with no HTTP listener; setSOLI_JOB_WORKERS=0onsoli serveso the web process only enqueues./__soli/jobsis the queue UI (list, cancel, retry) in--devand, in production, behind HTTP Basic (SOLI_JOBS_USER/SOLI_JOBS_PASSWORD) or a bearer token (SOLI_JOBS_TOKEN). Unconfigured production answers 404. The CLI andJob.*still work without the page. - Observability is opt-in rather than auto-instrumented everywhere. Prometheus counters at
/_metrics, health and readiness probes, structured JSON logs (SOLI_LOG_FORMAT=json), and OpenTelemetry traces (W3Ctraceparent+ OTLP/HTTP JSON export of the same span tree the dev-bar flamegraph uses) are all first-party — see Observability. So is error tracking: failed requests are grouped, redacted and triaged at/__soli/errorsfrom the app’s own database, where Rails, Laravel and Django reach for Sentry, Honeybadger or Flare — though HTTP failures only. Slow database queries get the same treatment at/__soli/slow_queries(grouped by shape, slowest runs kept with their binds), and new errors, regressions, error spikes and new slow queries can notify Slack, Teams, Discord, Google Chat, any webhook, email or a job of your own — basic alerting, without per-user routing or on-call schedules. What is still thinner than the big four: no auto-instrumentation of every third-party client library, no OTLP metrics/logs pipelines beyond traces, and no sampling UI — you point Soli at a collector (Jaeger, Grafana Tempo, Datadog agent, …) and configure sampling there.
Ecosystem
This is the honest big one: Rails has ~180k gems' worth of ecosystem, Laravel has first-party packages for billing, search and admin, Django has 15+ years of reusable apps. Soli's package registry is young. What's in the box is unusually broad — but if it's not in the box, you're writing it.
The basic package hygiene is in place, though, and on one axis Soli goes further than the three of them.
soli_version in soli.toml declares a minimum interpreter version
MSRV-style, like Cargo's rust-version or Bundler's
required_ruby_version, so a too-old soli fails fast with an upgrade
message rather than a cryptic runtime error. Prefix it with = and it becomes an
exact pin: soli inside that project fetches, verifies and
switches to that version, the way rustup reads a toolchain file — something Rails, Laravel and Django all
need a separate tool for (rbenv, asdf, pyenv). One field, no extra dotfile, and older soli binaries ignore the
pin instead of choking on it.
Head-to-Head
vs Ruby on Rails
Rails remains the DX benchmark, and Soli borrows its conventions deliberately.
Rails is ahead on: ActiveRecord maturity (multi-DB, richer association options, decades of edge cases), Hotwire, Devise/OmniAuth, ActiveAdmin, ActionMailbox/ActionText, system-test maturity (Capybara has a far deeper matcher and driver set than Soli's browser helpers), and the gem ecosystem.
Soli is ahead on: raw throughput and memory footprint by an order of magnitude, single-binary deployment, built-in profiling/N+1 tooling, document/e-invoice/signature capabilities, and zero-toolchain frontend.
vs Laravel
Laravel's first-party ecosystem (Horizon, Nova, Socialite, Cashier, Livewire, Octane) is the richest in the industry — Soli has no equivalents for most of it.
Soli is ahead on: performance (even against Octane), one-binary deploys with no PHP-FPM/composer/node stack, built-in realtime (rooms, presence, SSE) without Reverb or Pusher, and its testing/coverage runner.
vs Phoenix
Phoenix is the closest performance peer — and on the matched benchmark it beats Soli outright on all three write rows (1.3–1.6×) while tying it on database-read CPU, so treat "peer" literally rather than as a courtesy. Its LiveView still leads on node-granular diffs and forms. Soli's LiveView morphs the DOM in place with keyed reconciliation (widget state survives patches) and now has collection streams, reactive live queries, debounce/throttle, JS commands, in-socket soli-patch / soli-live navigation, nested child sockets, shared-state live_component assigns, send_update / child event == "update", client hooks, loading/click-away, and chunked HTTP soli-upload. Elixir's OTP supervision and distribution have no Soli equivalent — a nested component is not its own process.
Soli is ahead on: familiarity (Ruby-like syntax vs functional Elixir + the BEAM learning curve), the document/PDF suite, integrated dev tooling, and encrypted bundle distribution. If your product is realtime UI, choose Phoenix; if realtime is a feature, Soli's WebSockets/SSE cover it.
vs Django
Django's auto-admin remains its signature feature; Soli deliberately doesn't generate one — its 2026 bet is that agent-ready scaffolding produces a bespoke admin faster than you'd bend a generic one to your needs. Django's forms + validation framework is also deeper.
Soli is ahead on: performance (vs Python), built-in realtime (Channels is a bolt-on), modern developer tooling, and deployment weight.
vs the TypeScript full-stacks (AdonisJS, NestJS, Next.js)
Soli competes here on not having a node_modules directory: comparable expressiveness, far higher throughput, and one binary instead of a toolchain. It concedes the npm ecosystem and React-style rich-client UIs — though for a server-driven interface there is LiveView in the browser, and EUI for a native client that draws on the GPU without a document engine.
When to Choose Soli
Good fit today
- Server-rendered CRUD/SaaS apps with htmx-style interactivity, where throughput per server and operational simplicity are priorities
- Document-heavy back offices: invoicing, compliance (Factur-X), signed PDFs, spreadsheet import/export
- Products distributed on-premise or licensed, using encrypted
.solibundles or self-executing standalone binaries (no soli install on the target) - Teams who want Rails ergonomics without Ruby's runtime cost, and accept an integrated (SoliDB) stack
Reach for something else if you need
- Full SQL parity (raw SQL dialects, associations/
group_byon existing schemas) — SQL is document CRUD plus a column-aware mode for existing tables; SoliDB remains the integrated stack for graph/search/columnar - A generic, zero-build admin UI (Django-style) — Soli's bet is agents + scaffolding for bespoke admins instead
- A rich SPA toolchain — no bundler, no component framework, no
node_modules. For server-driven realtime UI there are now two answers (LiveView in the browser, EUI on a native client), but if your plan is React or Vue with a JSON API, Soli gives you the API and nothing else - A large third-party package ecosystem
Bottom Line
Soli's core loop — route, controller, model, view, test, deploy — is real, fast, and safer by default than most of its inspirations. Its standout edges (performance, dev tooling, documents, single-binary ops) are genuine and hard to replicate. Its gaps (SQL, ecosystem) are equally genuine, and some absences — the generic admin, the JS toolchain — are deliberate bets rather than debt. It is best understood as a young, opinionated, vertically-integrated Rails — already excellent inside its lane, and honest about the lane's current width.