| name | rails-wasm-author-constraints |
| description | Use this skill whenever checking if a Rails feature or gem works in WASM, or understanding
what's real vs. conceptual in lessons. Trigger on: 'does X work in WASM', 'can I use this
gem', 'gem compatibility', 'WASM limitation', 'what works', 'supported features', 'PGLite',
'threading', 'Net::HTTP', 'ActionCable', 'background jobs', 'can I teach X', 'bundle
install in WASM', 'conceptual vs real', or any Rails capability question — even without
mentioning WASM. Authoritative compatibility matrix: which features work, which are shimmed,
which are impossible, plus gem tiers, PGLite behavior, boot timing, and conceptual-vs-real
operations. General Rails knowledge is insufficient here. Do NOT use for file organization
(use rails-file-management) or frontmatter (use tutorial-lesson-config).
|
Rails WASM Author Constraints
What works, what doesn't, and how to design lessons around the WASM environment.
Quick Reference: What Can I Teach?
| Rails Feature | Works? | Notes |
|---|
| ActiveRecord (CRUD, queries, scopes) | Yes | Full support via PGLite |
| Migrations & schema management | Yes | Standard rails db:migrate |
| Controllers & routing | Yes | Full support |
| Views (ERB templates) | Yes | Full support |
| Form helpers & validations | Yes | Full support |
| Model associations | Yes | has_many, belongs_to, etc. |
| Scaffolding & generators | Yes | rails generate scaffold, etc. |
| Asset pipeline (Propshaft) | Yes | Importmaps, Stimulus, Turbo |
| Rails console | Yes | Interactive IRB via custom bridge |
| Authentication (basic) | Yes | has_secure_password, session-based |
| ActionMailer (define mailers) | Partial | Can define/configure but delivery is a no-op |
| Active Storage (upload) | Partial | Upload works but image processing is a no-op |
| Background jobs | No | Single-threaded; Solid Queue won't process |
| ActionCable / WebSockets | No | No IO.select, no real socket support |
| External HTTP requests | Yes | Net::HTTP and Faraday work via a JS fetch bridge; browser CORS applies, so many APIs need a proxy |
| Threads / parallel processing | No | Thread.new uses fibers (cooperative, single-threaded) |
| System commands from Ruby | No | system(), backticks, Open3 are non-functional |
Hard Limitations
These are impossible to work around in the WASM environment. Do not write lessons that depend on them:
No Raw Sockets
Socket operations (TCPSocket, UDPSocket, and all socket classes) fail because the underlying WASI syscalls are unimplemented. This rules out anything that needs a raw socket — connecting directly to an external database, a custom TCP protocol, etc.
Net::HTTP and Faraday are the exception: they're monkey-patched (lib/patches/http_bridge.rb) to route requests through a JS fetch bridge instead of opening a socket. See "Outbound HTTP" below.
No Process Spawning
system(), backticks, exec, fork, and Open3 are non-functional. The rails new generator works because it's been patched, but arbitrary shell commands from tutorial code will not work.
No Threading
Thread.new is shimmed to use Fiber.new (cooperative, single-threaded). Code that relies on parallel execution behaves differently. Background job processing and concurrent operations don't work as expected.
No IO.select
The poll_oneoff WASI syscall is unimplemented. This breaks gems like nio4r, Puma, and ActionCable's EventMachine adapter.
No chmod/fchmod
POSIX permission calls are stubbed. Avoid FileUtils.chmod in tutorial code. The rails new generator is pre-patched to handle this.
Outbound HTTP
Net::HTTP and Faraday work in WASM. lib/patches/http_bridge.rb prepends Net::HTTP#request (and registers a Faraday::Adapter::WasmHTTP) to call JS.global[:wasmHttpBridge].fetch() instead of opening a socket; lib/http-bridge.js runs the real browser fetch() on the other side.
Constraints
- CORS applies. The request runs as a browser
fetch(), so any API without permissive CORS headers will fail from the client side — this is most external APIs. Lessons that call third-party APIs typically need a CORS proxy in front of them.
- A CORS proxy is configurable, not built-in per-API: set
WasmHTTP::Connection.proxy_url and WasmHTTP::Connection.proxy_hosts to route matching hosts through a proxy (see packages/template/cors-proxy, a deployable Cloudflare Worker).
- No streaming. Responses are buffered in full before returning to Ruby; binary bodies are base64-encoded across the bridge.
- Single-threaded. Requests are serialized — no concurrent outbound calls.
- 30-second timeout per request (
AbortSignal.timeout(30000) in http-bridge.js).
Teaching It
A worked example ships at src/content/tutorial/9-outbound-http/1-making-http-requests/. For lessons that call real third-party APIs, still consider mocking the response for determinism/offline use — the bridge makes real calls possible, but a lesson that depends on a live third-party API's uptime and CORS policy is inherently less reliable than one that doesn't.
Gem Compatibility
Adding Gems
Authors can add gems to their tutorial by editing ruby-wasm/Gemfile and running npm run pack:wasm to repack the WASM binary (~1-4 min). This bakes all gems into the binary at pack time and regenerates the boot require list so the gem loads at boot.
Compatibility Tiers
| Tier | Description | Examples |
|---|
| Works | Pure Ruby gems, no native extensions | devise, friendly_id, pagy, pundit, draper, kaminari |
| Shimmed | Has native extensions but already patched | nokogiri (stub), io-console (stub), bcrypt |
| Needs testing | May work if extension compiles for WASM | Test with npm run pack:wasm + the smoke test |
| Won't work | Requires unsupported syscalls or networking | pg (native), mysql2, redis, sidekiq, puma |
Pre-Shimmed Gems
These gems are already handled by the WASM runtime:
| Gem | Behavior |
|---|
nokogiri | 165-line minimal stub; CSS selectors return []; sufficient for sanitization but not real HTML parsing |
io-console | Stubbed; winsize returns [80, 24]; raw yields without change |
nio4r | .so stripped; loads as empty shim |
date, psych, bigdecimal | .so stripped; Ruby falls back to pure-Ruby stdlib |
sqlite3 (native) | Replaced by PGLite adapter |
Gem Build Workflow
vim ruby-wasm/Gemfile
npm run pack:wasm
npm run dev
Database: PGLite
The database is PGLite — an in-browser PostgreSQL implementation compiled to WASM.
What Works
- Standard ActiveRecord operations:
create, find, where, update, destroy
- Migrations:
rails db:migrate, rails db:rollback
- Seeds:
rails db:seed
- PostgreSQL-compatible SQL syntax
- Multiple databases (development, test)
- Associations, joins, aggregations
- Indexes and constraints
What to Know
| Behavior | Detail |
|---|
| Data does not survive page reloads | WebContainer filesystem is in-memory; refreshing the browser resets everything |
| Database adapter | pglite (auto-configured by wasmify-rails; authors don't need to set this up) |
| Setup command | node scripts/rails.js db:prepare in prepareCommands |
| Location | pgdata/<dbname>/ in WebContainer filesystem |
| Performance | Slower than native PostgreSQL; acceptable for tutorial-sized datasets |
Lesson Design Implications
- Always include
['node scripts/rails.js db:prepare', 'Prepare development database'] in prepareCommands for lessons that use the database
- Provide seeds in templates so users start with data
- Don't rely on data from a previous lesson persisting — each lesson should set up its own state via migrations + seeds
Boot Timing
The WASM runtime takes time to load. Set expectations for tutorial users:
| Phase | Typical Duration | What Happens |
|---|
npm install | 10-30s | Downloads ~80MB WASM binary + dependencies |
| WASM compile | 2-5s | Browser compiles the binary |
| Rails bootstrap | 2-5s | Loads Rails framework from embedded VFS |
| Command execution | Varies | User's command runs |
Total first-load time: 15-40 seconds depending on network and browser.
Author Tips for Boot Experience
- Include a note in early lessons: "The Ruby runtime takes a moment to load — this is normal!"
- Use
prepareCommands with descriptive labels so users see progress
- The
['output', 'Setup Logs'] terminal panel shows boot details
- Subsequent lesson navigation is faster if the WebContainer is already booted
Auto-Authentication
The runtime includes an auto-login patch: if tmp/authenticated-user.txt exists in the Rails app root, the first HTTP request auto-authenticates the user found by User.find_by(email_address: <file contents>). The file should contain a single email address (e.g., admin@example.com).
To use: Place workspace/tmp/authenticated-user.txt in _files/ or a template, containing the email of a seeded user. The first request calls start_new_session_for(user), creating a session cookie for all subsequent requests. Runs once per VM lifetime (the $__pre_authenticated global flag prevents repeat attempts).
This requires the Rails 8 Authentication concern and a User model with an email_address column.
Filesystem Boundaries
Ruby code can only access /workspace (the WASI preopen). Attempting to access paths outside this boundary raises Errno::ENOENT. Tutorial code should never navigate above /workspace with Dir.chdir("..") or absolute paths outside the preopen.
Conceptual vs. Real Operations
Some tutorial operations are conceptual — they teach the correct pattern but the actual execution requires something different in the WASM environment:
| Operation in lesson content | Reality | What the author must do |
|---|
"Add gem 'devise' to your Gemfile" | Gems are baked into the WASM binary at build time | The gem must already be in ruby-wasm/Gemfile and repacked with npm run pack:wasm before the tutorial is published |
"Run bundle install" | bundle install is a no-op in WASM — all gems come from the binary | Include it for pedagogical completeness; it will appear to succeed |
"Run rails server" | The Rails server runs through node scripts/rails.js server | Use node scripts/rails.js server in frontmatter mainCommand; in lesson content, show rails server since that's what the terminal wrapper understands |
"Edit database.yml" | Database adapter is PGLite, auto-configured by wasmify-rails | Pre-configure in the template; showing a database.yml edit is fine for teaching but won't change the runtime behavior |