What is implemented

Altair is built in phases, each ending with something working and visible. Everything below is implemented, tested and shipped. Each area has a guide: Routing, Controllers, Views, Record, Sessions and auth, Configuration, Security and Uploads.

Phase 0 — Foundation#

The core request/response loop: Altair::Application, request handling, error pages and configuration.

Phase 1 — Router#

  • Segment-based routing (not regex)
  • resources blocks with member, collection and nested resources
  • Constraints and implicit format suffix
  • redirect, glob segments and singular resource

Phase 2 — Controllers#

A type-checked controller base with actions, parameters and rendering hooks. A wrong method on a dispatch is a compile error, not a runtime 500. The controller layer gained a hardening wave:

  • render json: serializes any JSON-able object, redirect_back with open-redirect protection
  • request.format (path suffix, then Accept, then :html) and JSON request bodies merged into params
  • head answers bodyless; no_content for a bare 204
  • respond_to — one action, several format handlers, undeclared formats answer 406
  • before_action / after_action with only: / except:, skip_before_action / skip_after_action, inheritance across the hierarchy
  • rescue_from maps exceptions to handler responses (inherited, only:-filtered, subclass-aware)
  • stream opens chunked bodies for large responses and server-sent events
  • A segment-based route index buckets routes by their first segment so matching only tests viable candidates

Phase 3 — Views#

  • ECR templates
  • Auto-escaping by default — XSS-safe out of the box
  • Layouts with yield, partials (render "form")
  • Helpers: link_to, content_tag, a basic form builder, and an htmx layer

Phase 4 — Record (ORM)#

Three vertical waves:

  • Wave 1: adapter interface + SQLite3, connection pool, migrations DSL + runner, auto-regenerated db/schema.cr
  • Wave 2: CRUD + finders (find_by_*), validations (valid? + errors), timestamps and callbacks
  • Wave 3: associations (belongs_to, has_many, has_one) with batched eager loading via Relation#includes, and dependent: handling

Phase 5 — CLI + Generators#

  • altair new generates a standard project layout
  • altair g model|migration|controller|scaffold generates ready-to-edit files
  • App commands (server, routes, db:migrate / db:rollback / db:seed) run from anywhere inside a project — no bin/ prefix needed
  • altair install copies a built binary onto your PATH, prints its SHA-256 digest, and refuses to clobber unrelated files without --force
  • altair update checks GitHub for a newer release, verifies the checksum and swaps the binary atomically
  • Prebuilt binaries for Linux, macOS and Windows (amd64 + arm64) published with checksums; verified one-command installers

Phase 6 — Hardening#

Sessions, authentication, file-driven configuration, multipart uploads and a security middleware set, shipped in waves:

  • Sessions + flash + CSRF — signed-cookie sessions with a session hash view, one-request flash messages, protect_from_forgery with constant-time token verification, and login helpers (sign_in, sign_out, require_login, authenticate!, current_user_id)
  • JWT auth — Altair::Auth::JWT, a minimal HS256 implementation for stateless API auth
  • Configuration — .env (real env vars win; .env.<environment> overrides .env) and config/database.yml per-environment settings, merged into config at boot by Altair::Config::DotEnv / Altair::Config::Database; altair new generates both files
  • Multipart uploads — multipart/form-data bodies parse into the parameter bag (scalar fields as params, files as Altair::HTTP::UploadedFile via params.upload("avatar"), with UploadedFile#save + #content)
  • Security middleware set — SecurityHeaders (default nosniff / SAMEORIGIN / referrer policy, driven by config.security_headers), RequestId (request.request_id, echo-back through config.request_id_header, appended to the request log line), RateLimit (sliding-window, config.rate_limit — MemoryStore / RedisStore, per-path rules, X-RateLimit-* + Retry-After), and opt-in Cors (config.cors.origins enables it; preflight answered directly)

Phase 7 — Post-release#

Four features shipped, demonstrated end to end in examples/blog:

  • Testing utilities — Altair::Test.boot(App) binds an ephemeral port (with a configure: hook for per-spec settings such as secret_key_base), plain get/post/post_json/put/patch/ delete helpers for one-off requests, and the cookie-jar Altair::Test::Client whose session survives between requests with browser-like redirect following. Database helpers: migrate! applies pending migrations through the same engine the CLI drives, and transactional { } wraps an example in an always-rolled-back transaction (nested calls join through savepoints)
  • Full authentication — altair g auth [User] writes a complete registration/login stack: User model with unique email, a migration carrying the unique index, sessions and registrations controllers, login/register views and the /login, /register, /logout routes. The password_auth model macro stages plain passwords, hashes them into a digest column through PBKDF2-HMAC-SHA256 (Altair::Auth::PasswordHasher — OpenSSL stdlib, no new dependencies), validates length and confirmation as ordinary record errors, and gates logins behind authenticate_password
  • Asset pipeline — assets/ sources compile into fingerprinted files under public/assets/<name>-<sha256>.<ext> plus a manifest.json via altair assets:precompile; stylesheet_link_tag, javascript_asset_tag and asset_url resolve through the manifest when compiled and fall back to plain copies in development; fingerprinted responses carry Cache-Control: immutable
  • Background jobs — job classes declare typed parameters once (params user_id : Int64) and get compile-checked enqueue, enqueue_in and enqueue_at. Jobs persist in a lazily-created altair_jobs table (no migration needed), claiming is atomic so concurrent workers never double-run a row, failures retry with exponential backoff inside a per-job budget, altair jobs:work runs the worker with graceful shutdown, altair jobs:stats prints status counts, and test mode collects enqueues for synchronous draining

Performance hardening#

  • find_each streams bounded batches that keep the scoped where filters and includes preloaders
  • Relation#count / size run COUNT(*) without materializing rows
  • The server resizes the execution context to the available workers on boot (honors CRYSTAL_WORKERS; config.parallel_execution opt-out)
  • Warm connection-pool defaults: initial 2 / idle 2 / max 10
  • A development-mode N+1 detector warns on identical SQL fired more than config.n_plus_one_threshold times in one request
  • A database admission-control gate (config.db_max_active_queries, off by default) parks excess request fibers on a FIFO semaphore outside the pool, bounding tail latency under saturation
  • The record hot path builds each SQL statement once per connection (Connection#sql_template): find, find_by_* and insert cache their quoted statements, halving the write-path allocations (PostgreSQL: Item.create 2,033 → 964 B/op on the frozen-GC harness) — see Benchmarks for the end-to-end effect

Testing and quality#

  • 992 specs passing (15 pending: 7 Redis rate-limit + 1 Redis middleware + 6 PostgreSQL concurrency contract; the full PostgreSQL contract suite runs when ALTAIR_TEST_PG_URL is set)
  • Formatter clean, linter silent on framework sources
  • Smart error pages: 404 with route suggestions, 405 with _method explanation, detailed 500 diagnostics in debug mode only

Rich query DSL completion + advanced associations (closing Phase 7 wave)#

  • Joins — Post.all.joins(:comments).where("comments.body", "altair") emits a real INNER JOIN with table-qualified where; has_many joins auto-enable SELECT DISTINCT and COUNT(DISTINCT pk); left_joins keeps unmatched owners
  • has_many :through — has_many :tags, through: :post_tags works lazily (one JOIN per owner), eagerly (includes(:tags) batches all owners into one JOIN), and composes with joins; source association is inferred from the name — explicit source: only when ambiguous
  • Polymorphic — belongs_to :commentable, polymorphic: true + has_many :comments, as: :commentable with batched-per-type eager loading and dependent: :destroy/:nullify; t.references :x, polymorphic: true migration helper generates the id/type pair plus composite index

ORM hardening (Wave 0)#

  • IN (...) chunking — eager loading splits oversized id lists at 500 binds, so large collections never trip SQLite's variable limit
  • Custom primary keys — table :posts, primary_key: :uuid; string PKs auto-generate SecureRandom.uuid before insert
  • Ordering — order accumulates (order(:a).order(:b) = ORDER BY a, b); reorder replaces; unscope_order clears
  • Reloading — Relation#reload re-runs the query; Model#reload re-reads attributes from the database
  • Atomic writes — db/schema.cr written via tmp+rename; db:migrate acquires PG advisory lock against concurrent races

Phase 8 — Database ergonomics#

Completed — altair new -d postgresql wires the pg shard, postgres:// URLs and the adapter require; bin/altair db:create / db:drop manage every env in config/database.yml; ENV["DATABASE_URL"] overrides at boot and in those commands.

v0.4.0 — ORM completion wave#

  • Query DSL: where_not, or_where, :like / :in / :null / :not_null operators; finders first / last / take / ids / pick / exists? / any? / none?; bulk update_all / delete_all
  • Lifecycle: after_commit / after_rollback hooks; direct-write touch / increment! / decrement!
  • Associations & validations: counter_cache, batched dependent: :destroy, conditional validations (if: / unless: / allow_nil:) and case_sensitive: false uniqueness

Phase 10 — Router & future features#

  • Dynamic redirect: redirect "/t/:id", to: "/tweets/:id" interpolates route params into Location headers
  • Cache layer: MemoryStore (dev) + RedisStore (production); Altair.cache.fetch with TTL expiry
  • Storage abstraction: DiskStore (local) + S3Store (AWS SigV4); shared upload/delete/url contract; has_one_attached macro for model attachments via lazy altair_attachments table
  • WebSocket Cable: channel-based broadcaster at /cable with auth hook (cable_auth), heartbeat ping/pong, JSON envelope protocol, automatic subscriber cleanup
  • API mode: altair new --api generates JSON-only project (no views/assets) with CORS enabled
  • Admin generator: altair g admin Post writes namespaced controller with require_login + routes
  • Observability: /health and /metrics (Prometheus format) behind config.observability = true
  • Structured logs: config.structured_logs = true emits one JSON object per request
  • Redis client: Pure-Crystal Altair::Redis::Client built from scratch — RESP2, connection pool, pub/sub, pipeline, transactions. No external Redis shard needed.

Phase 9 — CLI ergonomics#

Planned — altair g job and destroy generators, per-environment database helpers, server flags, and project diagnostics.

What is planned next#

  • Phase 12 candidate: multi-tenancy
  • Email sending