class ApplicationController < ActionController::Base
before_action :set_turbo_cache_control
class_attribute :turbo_no_cache, default: false
def self.no_turbo_cache
self.turbo_no_cache = true
end
private
def set_turbo_cache_control
return unless self.class.turbo_no_cache
@turbo_cache_control = "no-cache"
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private"
response.headers["Pragma"] = "no-cache"
end
end
class AccountsController < ApplicationController
before_action :authenticate_user!
no_turbo_cache
def show
@account = current_user.account
@invoices = @account.invoices.recent.limit(20)
end
def edit
@account = current_user.account
end
def update
@account = current_user.account
if @account.update(account_params)
redirect_to account_path, notice: "Account updated."
else
render :edit, status: :unprocessable_entity
end
end
private
def account_params
params.require(:account).permit(:name, :billing_email, :company)
end
end
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Dashboard" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<% if @turbo_cache_control.present? %>
<meta name="turbo-cache-control" content="<%= @turbo_cache_control %>">
<% end %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= render "shared/flash" %>
<%= yield %>
</body>
</html>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.boundClear = this.clear.bind(this)
document.addEventListener("turbo:before-cache", this.boundClear)
}
disconnect() {
document.removeEventListener("turbo:before-cache", this.boundClear)
}
// Called by the sign-out button before the DELETE request fires.
signOut() {
this.clear()
}
clear() {
if (window.Turbo?.cache) {
window.Turbo.cache.clear()
}
if (window.Turbo?.session) {
window.Turbo.session.clearCache()
}
}
}
Turbo Drive keeps a client-side cache of visited pages so that back/forward navigation and restoration renders feel instant. It shows a cached snapshot immediately, then fetches a fresh copy in the background. That behavior is great for public pages but dangerous for authenticated ones: after a user logs out, pressing Back can flash the previous user's account page from the in-memory snapshot cache, leaking data on shared machines. This snippet shows the two coordinated defenses needed to fully suppress that.
The ApplicationController sets an explicit opt-out on every response that renders sensitive data. set_turbo_cache_control inspects a class-level turbo_no_cache flag and, when present, emits <meta name="turbo-cache-control" content="no-cache"> via a helper-populated @turbo_cache_control and also sends HTTP Cache-Control: no-store headers. The no_turbo_cache macro lets individual controllers declare themselves non-cacheable, which is why AccountsController calls it at the top. The turbo-cache-control meta tag is the canonical Turbo mechanism: no-cache tells Turbo to skip showing a preview snapshot for this page, so a stale render never appears even briefly.
The layout in application.html.erb renders @turbo_cache_control into the document head only when set. Turbo reads this meta tag on each navigation, so it must be present in the served HTML rather than injected later.
The meta tag alone is not enough, because Turbo may have already cached earlier snapshots before logout. The logout controller Stimulus controller calls Turbo.cache.clear() on beforeCache and immediately when logout is triggered, wiping every stored snapshot. Turbo.session.clearCache() is invoked defensively as well. Wiring it to the sign-out button guarantees the cache is emptied at the exact moment the session ends.
A key trade-off is that no-cache disables the instant-preview optimization for those pages, so navigation feels slightly slower — an acceptable cost for account, billing, or admin screens. The pitfall to avoid is relying only on HTTP headers: Turbo's snapshot cache lives in memory and ignores Cache-Control, which is why the meta tag and explicit clear() are both required. Reach for this pattern on any route rendering per-user data behind authentication.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.