How to build a real-time app with Crystal and Kemal
The core constraint here isn't just the real-time nature; it's server-side confidentiality. The server must physically refuse to send one participant's data to another until the reveal phase. This isn't a UI trick; it's a server invariant.
Why Kemal?
I chose Kemal because it's a Sinatra-style micro-framework. It provides routes, WebSockets, and request/response handling—and that's it. Since this app is ephemeral (state lives in memory and expires after a few hours), an ORM or a complex asset pipeline would just be dead weight.
My rule: the framework is just an adapter. All the business logic lives in plain Crystal objects that don't even import Kemal. This makes the domain logic easy to test without booting a server.
Domain Logic from Scratch
Instead of using generic arrays, I use value objects to enforce invariants. For example, a ranking must contain exactly ten unique cards. By validating this at construction, the rest of the app can trust the data implicitly.
struct Ranking
getter order : Array(Motivator)
def initialize(@order : Array(Motivator))
validate!
end
# 1 is most important, 10 is least.
def position(motivator : Motivator) : Int32
@order.index!(motivator) + 1
end
private def validate!
unless @order.size == Motivator.values.size
raise ArgumentError.new("A ranking must order all ten cards")
end
if @order.uniq.size != @order.size
raise ArgumentError.new("A ranking must not contain a duplicate card")
end
end
endState Machine Control
The session is managed as a two-phase state machine: Lobby → Revealed. Once a session is revealed, it's terminal. I use guard methods to ensure no rules are bypassed.
def reveal(token : String) : Nil
raise Forbidden.new("Only the facilitator can reveal") unless token == @facilitator_token
guard_lobby!
@phase = Phase::Revealed
end
private def guard_lobby! : Nil
raise WrongPhase.new("Session is already revealed") if revealed?
endBy passing the current time into the session logic from the caller, the expiry logic remains deterministic and trivial to test. This approach turns the server into a strict gatekeeper of data, ensuring privacy is maintained until the exact moment of reveal.