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)
resourcesblocks withmember,collectionand nested resources- Constraints and implicit format suffix
redirect, glob segments and singularresource
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_backwith open-redirect protectionrequest.format(path suffix, thenAccept, then:html) and JSON request bodies merged intoparamsheadanswers bodyless;no_contentfor a bare 204respond_to— one action, several format handlers, undeclared formats answer 406before_action/after_actionwithonly:/except:,skip_before_action/skip_after_action, inheritance across the hierarchyrescue_frommaps exceptions to handler responses (inherited,only:-filtered, subclass-aware)streamopens 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 viaRelation#includes, anddependent:handling
Phase 5 — CLI + Generators#
altair newgenerates a standard project layoutaltair g model|migration|controller|scaffoldgenerates ready-to-edit files- App commands (
server,routes,db:migrate/db:rollback/db:seed) run from anywhere inside a project — nobin/prefix needed altair installcopies a built binary onto yourPATH, prints its SHA-256 digest, and refuses to clobber unrelated files without--forcealtair updatechecks 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
sessionhash view, one-requestflashmessages,protect_from_forgerywith 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) andconfig/database.ymlper-environment settings, merged intoconfigat boot byAltair::Config::DotEnv/Altair::Config::Database;altair newgenerates both files - Multipart uploads —
multipart/form-databodies parse into the parameter bag (scalar fields as params, files asAltair::HTTP::UploadedFileviaparams.upload("avatar"), withUploadedFile#save+#content) - Security middleware set —
SecurityHeaders(defaultnosniff/SAMEORIGIN/ referrer policy, driven byconfig.security_headers),RequestId(request.request_id, echo-back throughconfig.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-inCors(config.cors.originsenables 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 aconfigure:hook for per-spec settings such assecret_key_base), plainget/post/post_json/put/patch/deletehelpers for one-off requests, and the cookie-jarAltair::Test::Clientwhose session survives between requests with browser-like redirect following. Database helpers:migrate!applies pending migrations through the same engine the CLI drives, andtransactional { }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,/logoutroutes. Thepassword_authmodel 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 behindauthenticate_password - Asset pipeline —
assets/sources compile into fingerprinted files underpublic/assets/<name>-<sha256>.<ext>plus amanifest.jsonviaaltair assets:precompile;stylesheet_link_tag,javascript_asset_tagandasset_urlresolve through the manifest when compiled and fall back to plain copies in development; fingerprinted responses carryCache-Control: immutable - Background jobs — job classes declare typed parameters once
(
params user_id : Int64) and get compile-checkedenqueue,enqueue_inandenqueue_at. Jobs persist in a lazily-createdaltair_jobstable (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:workruns the worker with graceful shutdown,altair jobs:statsprints status counts, and test mode collects enqueues for synchronous draining
Performance hardening#
find_eachstreams bounded batches that keep the scopedwherefilters andincludespreloadersRelation#count/sizerunCOUNT(*)without materializing rows- The server resizes the execution context to the available workers on boot (honors
CRYSTAL_WORKERS;config.parallel_executionopt-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_thresholdtimes 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_*andinsertcache their quoted statements, halving the write-path allocations (PostgreSQL:Item.create2,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_URLis set) - Formatter clean, linter silent on framework sources
- Smart error pages: 404 with route suggestions, 405 with
_methodexplanation, 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-qualifiedwhere;has_manyjoins auto-enableSELECT DISTINCTandCOUNT(DISTINCT pk);left_joinskeeps unmatched owners has_many :through—has_many :tags, through: :post_tagsworks lazily (one JOIN per owner), eagerly (includes(:tags)batches all owners into one JOIN), and composes withjoins; source association is inferred from the name — explicitsource:only when ambiguous- Polymorphic —
belongs_to :commentable, polymorphic: true+has_many :comments, as: :commentablewith batched-per-type eager loading anddependent: :destroy/:nullify;t.references :x, polymorphic: truemigration 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-generateSecureRandom.uuidbefore insert - Ordering —
orderaccumulates (order(:a).order(:b)=ORDER BY a, b);reorderreplaces;unscope_orderclears - Reloading —
Relation#reloadre-runs the query;Model#reloadre-reads attributes from the database - Atomic writes —
db/schema.crwritten via tmp+rename;db:migrateacquires 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_nulloperators; findersfirst/last/take/ids/pick/exists?/any?/none?; bulkupdate_all/delete_all - Lifecycle:
after_commit/after_rollbackhooks; direct-writetouch/increment!/decrement! - Associations & validations:
counter_cache, batcheddependent: :destroy, conditional validations (if:/unless:/allow_nil:) andcase_sensitive: falseuniqueness
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.fetchwith TTL expiry - Storage abstraction: DiskStore (local) + S3Store (AWS SigV4); shared upload/delete/url contract;
has_one_attachedmacro for model attachments via lazyaltair_attachmentstable - WebSocket Cable: channel-based broadcaster at
/cablewith auth hook (cable_auth), heartbeat ping/pong, JSON envelope protocol, automatic subscriber cleanup - API mode:
altair new --apigenerates JSON-only project (no views/assets) with CORS enabled - Admin generator:
altair g admin Postwrites namespaced controller withrequire_login+ routes - Observability:
/healthand/metrics(Prometheus format) behindconfig.observability = true - Structured logs:
config.structured_logs = trueemits one JSON object per request - Redis client: Pure-Crystal
Altair::Redis::Clientbuilt 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