# Rails Wasm Author Constraints

> 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).

- Skill: `palkan/rails-wasm-author-constraints` (Agent Skill)
- Install (CLI): `npx skillmds@latest add palkan/rails-wasm-author-constraints`
- Raw SKILL.md: https://api.skillmd.com/api/skills/palkan/rails-wasm-author-constraints/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: palkan (https://skillmd.com/u/palkan)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/palkan/rails-wasm-author-constraints

---


# 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 | No | No outbound networking from Ruby — sockets are unimplemented at the WASI level |
| 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 Outbound Networking

Socket operations (`TCPSocket`, `UDPSocket`, and all socket classes) fail because the underlying WASI syscalls are unimplemented. `Net::HTTP` and `open-uri` will raise errors when attempting connections. You cannot:
- Call external APIs from Ruby
- Download files from the internet
- Connect to external databases or services

**Workaround for lessons:** If you want to teach API consumption, focus on the controller/model patterns and mock the responses. Show the code structure without executing real HTTP calls.

### 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.

## Gem Compatibility

### Adding Gems

Authors **can add gems** to their tutorial by editing `ruby-wasm/Gemfile` and running `bin/build-wasm` to rebuild the WASM binary. This bakes all gems into the binary at build time.

### 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 `bin/build-wasm` |
| **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

```bash
# 1. Edit the Gemfile
vim ruby-wasm/Gemfile

# 2. Rebuild the WASM binary (takes several minutes)
bin/build-wasm

# 3. Test your tutorial locally
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.

## Integration Testing

Rails integration tests work in the WASM runtime with important limitations.

### What Works

- `ActionDispatch::IntegrationTest` with `get`, `post`, `patch`, `delete`
- `assert_response`, `assert_redirected_to`
- `assert_difference`, `assert_no_difference`
- Fixtures (`fixtures :all`) for test data
- Cookie-based session management for authentication

### What Does NOT Work

**Nokogiri-dependent assertions are unavailable.** The Nokogiri gem is a minimal stub in WASM — CSS selectors return `[]` and HTML parsing is non-functional. This means these Rails test helpers will not work:

- `assert_select` — relies on Nokogiri CSS selectors
- `css_select` — same
- `assert_dom` — same
- Any assertion that parses response HTML into a DOM

### Best Practices

**Use `response.body` string matching instead of DOM assertions:**

```ruby
# BAD — uses Nokogiri under the hood
assert_select "h1", text: "Tickets"
assert_select ".alert--error", text: /blank/
assert_select "form"

# GOOD — plain string matching
assert_includes response.body, "Tickets"
assert_includes response.body, "blank"
assert_includes response.body, "<form"
```

**Define an `assert_text` helper in `test_helper.rb`:**

```ruby
class ActionDispatch::IntegrationTest
  def assert_text(text)
    assert_includes response.body, text
  end
end
```

**Use cookie-based sign_in without making HTTP requests:**

```ruby
class ActionDispatch::IntegrationTest
  private

  def sign_in(user)
    Current.session = user.sessions.create!

    ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
      cookie_jar.signed[:session_id] = Current.session.id
      cookies[:session_id] = cookie_jar[:session_id]
    end
  end
end
```

**Disable test parallelization** — we don't have threads or processes:

```ruby
module ActiveSupport
  class TestCase
    # parallelize(workers: 1)
    fixtures :all
  end
end
```

## 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 rebuilt with `bin/build-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 |

