September 15, 2021·6 min read·engineeringrubyrailsjavascriptencryption

Rails 7 alpha makes JavaScript preprocessing an application choice

Rails 7's first alpha introduces import maps as a default while retaining direct bundler integration. Active Record encryption makes another boundary explicit by declaring sensitive attributes in the model.

A server-rendered Rails application does not necessarily need to transform every JavaScript module before a browser can run it. Rails 7.0 Alpha 1, released on September 15, turns that observation into a default based on import maps and Hotwire. Applications that need a JavaScript compilation step retain an explicit bundler path.

The significant design choice is where Rails integrates with JavaScript. Import maps let browser-ready modules retain logical names while the asset system supplies their locations. A bundler-based application instead hands built output to Rails' asset pipeline. The framework can support both arrangements without asking every application to use the same preprocessing machinery.

I find Ruby's readable conventions useful when they remove decisions that do not help explain an application. The new default is attractive for code that can run as ordinary modules. That judgement does not require pretending that applications using syntax transformations have the same needs.

Import maps separate names from asset locations

The importmap-rails documentation published in version 0.5.3 describes mapping logical JavaScript imports to versioned or digested files. Application code can import a stable name while the map identifies the URL to fetch. Rails' asset pipeline remains responsible for the files served through that arrangement.

This addresses a concrete interaction with caching. An import that embeds another module's fingerprinted filename changes when that filename changes. A map can hold the changing location separately, allowing the importing source to retain its logical reference. The architecture does not require combining every module into one artifact to give the browser a resolvable location.

The documentation places the application entry point in app/javascript/application.js and the map configuration in config/importmap.rb. A layout helper supplies the configuration and starts the entry module. Development reloads the configuration when it changes, with a documented server-restart requirement when removed pins must disappear from generated configuration and preload lists.

Those details keep the change concrete. JavaScript still has an entry point, dependencies still need resolvable locations, and the page still needs to load the configuration. Removing the preprocessing stage does not remove the module graph. It gives the browser more direct responsibility for loading that graph.

Package tooling supplies browser-ready URLs

The importmap tooling can ask JSPM's API to resolve package dependencies and add pins. A request for a package can introduce pins for its own dependencies as well. The documented CLI supports selecting a package version and choosing among supported CDN providers.

For example, this command shape pins a particular package version. It is an authoring command that changes the application's map, not code to run on each page request.

./bin/importmap pin react@17.0.1

Application JavaScript can then use the logical name in an ordinary module import. The map supplies its browser-resolvable location.

import React from "react"

This does not compile JSX. A dependency being available through a JavaScript CDN and application syntax requiring transformation are different issues. The documented pinning workflow resolves package locations. It does not turn the browser into a compiler for every syntax extension used by application code.

The same documentation allows downloading compiled dependencies and keeping them inside the application with local pins. That makes remote hosting a choice within the import-map approach. It also makes clear that a package must arrive in a form suitable for this module-loading model, whether the bytes come from an external URL or the application's assets.

Preloading addresses dependency discovery

Loading modules separately introduces a discovery problem when one module imports another that imports a third. The browser can otherwise learn about each dependency only after fetching the preceding source. The documented integration preloads pinned modules by default and permits disabling that behavior for an individual pin.

This is a useful example of how the new approach still has performance mechanisms. Avoiding a bundle does not mean ignoring load order or resource discovery. The map supplies known locations, while preload hints make selected resources available earlier in the loading process. The desired set depends on the application's module use.

The contemporary documentation also distinguishes native import-map support in Chrome and Edge from the shim used for other ESM-capable browsers. It would be inaccurate to describe native support as universal on this date. The Rails integration includes the shim as part of making the chosen authoring model usable across those environments.

Bundled output has a defined handoff to Rails

David Heinemeier Hansson's September 6 explanation describes the alternate bundling arrangement. JavaScript source stays under app/javascript, package scripts run the build, and output reaches app/assets/builds. Rails can then apply asset digesting and serving conventions to those results.

The companion jsbundling integration supports esbuild, Rollup and Webpack. Its role is the handoff between the chosen JavaScript build and Rails assets. Application developers remain responsible for managing the JavaScript dependencies and build configuration appropriate to their application.

That interface is a clearer responsibility boundary than assuming every JavaScript concern needs a Ruby abstraction. A project requiring a compiler can configure one using its normal ecosystem conventions. A project using browser-ready modules can avoid introducing that stage simply to satisfy the framework default.

The alpha announcement specifically asks for feedback because the frontend change is substantial. It is an early release for evaluating that direction. The date does not justify claims about how the final Rails release will behave or how every existing Webpacker application will migrate.

Encrypted attributes make another application boundary explicit

The same alpha includes Active Record encryption. Its guide in the alpha tag describes application-level encryption between model access and database storage. The application reads ordinary values, while the database stores encrypted representations for declared attributes.

This is distinct from encrypting the database's storage device. A database copy can contain ciphertext for those fields without automatically containing the application's ability to decrypt it. The protection still depends on key handling and on where the application exposes clear values. Model-level encryption does not make a compromised application process unable to use the keys it needs.

A model expresses the policy through an attribute declaration. The alpha's EncryptableRecord implementation adds the declaration interface and registers an encrypted attribute type.

class Article < ApplicationRecord
  encrypts :title
end

With the required keys and database column configured, assigning and reading title uses the ordinary attribute interface. Persistence invokes the encryption layer. The declaration also identifies the sensitive attribute at the code level, giving related framework behavior a place to learn that policy.

Queryability changes the encryption tradeoff

The default encryption mode is nondeterministic, so encrypting the same value repeatedly produces different ciphertexts. That prevents using a single encrypted representation as an ordinary equality lookup key. The guide therefore provides a deterministic option when querying the encrypted attribute is required.

Deterministic encryption makes equal values yield matching representations under the same scheme. That enables equality-style queries but reveals equality relationships that randomized representations would hide. The guide recommends retaining the default unless queries require the alternative. This is a real data-model decision, even though the option is a short addition to the model declaration.

Encrypted representations also consume more column space because they include encoded data and metadata. Existing column sizes must account for the representation being stored. Transparent accessors do not make the persisted value the same size or preserve every operation available over plaintext.

The guide includes a transition mode for reading existing unencrypted data and a separate extended-query setting for finding both representations. It presents those as migration support, with stricter defaults as the intended destination. That distinction matters for existing applications because adding a declaration does not rewrite every historical row immediately. The read policy and the conversion of stored values need to describe the same stage of the migration.

I regard these explicit declarations as useful when their limits remain visible. Choosing import maps says the application can consume browser-ready modules. Choosing deterministic encryption says queries require a stable representation. Rails provides the integration in each case, while the application supplies the requirement that makes the tradeoff appropriate.

The alpha also makes Zeitwerk the exclusive autoloader. Together, these changes show a release revising established framework defaults while exposing specific integration choices. On September 15, the concrete opportunity is to evaluate those choices in the alpha, including whether an application's JavaScript needs preprocessing and which encrypted attributes need queryable representations.