A Ruby port of Elixir's Ash Framework - declarative, resource-oriented domain modeling.
Add to your Gemfile:
gem "archema", path: "~/src/archema" # Local developmentThen bundle install. For CLI access: bundle binstubs archema
Archema provides a declarative approach to building Ruby applications where resources are authoritative and infrastructure (migrations, APIs, etc.) is derived from resource definitions.
Archema is built on a key insight: we can unify the best of three paradigms into a single resource-oriented model.
| Paradigm | What Archema Takes |
|---|---|
| RDBMS | Relationship semantics, JOIN efficiency, referential integrity, ACID transactions |
| Document stores | Schema expressiveness (JSON Schema oneOf/anyOf/dependentRequired), versioning, readability |
| Event sourcing | Temporal awareness, immutable audit trails, as_of queries, projection rebuilds |
This isn't about replacing any paradigm—it's about a unified resource definition that projects appropriately to each storage layer. Document schemas (JSON/YAML) have richer constraint vocabulary than RDBMS; Archema uses that as the canonical constraint model and projects down to SQL where possible.
See ADR-003: Document-Schema-First for the architectural rationale.
The result: schema changes as safe as code changes.
Key ideas:
- Resource definition is the single source of truth — storage layers are intelligent servants
- Document-schema-first constraints — JSON Schema vocabulary, RDBMS gets best-effort projection + triggers
- Automatic expand-contract transitions — rename a field, both names work during transition
- Branch-safe schema changes — feature branches and production coexist safely
- Temporal awareness —
as_of(time)queries, full version history - Cross-store composition — same Resource writing to RDBMS + read replica + JSONL audit trail
- Cross-store relationships — JOIN across databases, even across store types (e.g., YAML frontmatter files joined against PostgreSQL)
# The vision: declare intent, everything else is automatic
class AgentCard < Archema::Resource
uuid_primary_key :id
field :full_name, :string, was: :name # Rename with automatic transition
end
# Old code using :name continues to work
# Storage layer syncs writes, monitors usage, contracts when safeclass User < Archema::Resource
uuid_primary_key :id
field :email, :string, constraints: { format: /@/ }
field :name, :string
field :bio, :string, :optional # Allow nil
field :role, :atom, default: :reader, constraints: { one_of: [:admin, :author, :reader] }
field :api_key, :string, :optional, :sensitive # Redacted in logs
timestamps
relationships do
has_many :posts, Post
belongs_to :organization, Organization, :optional # Optional relationship
end
identities do
identity :unique_email, [:email]
end
actions do
defaults [:create, :read, :update, :destroy]
end
policies do
authorize_if :always
end
endFields and relationships use flag symbols for common options:
| Flag | Fields | Relationships | Effect |
|---|---|---|---|
:optional |
✓ | belongs_to |
Allow nil values (FK nullable) |
:required |
✓ | belongs_to |
Disallow nil (default for fields and belongs_to) |
:private |
✓ | Exclude from serialization | |
:sensitive |
✓ | Redact in logs, exclude from tool export | |
:immutable |
✓ | Write-once: settable on create, then locked | |
:readonly |
✓ | No setter (usually prefer generated fields instead) |
|
:mutable |
✓ | Explicitly allow updates (overrides default PK immutability) |
Flags can be combined: field :secret, :string, :optional, :private, :sensitive
Cardinality Constraints: has_one/has_many/many_to_many support min_required/max_allowed. The :required flag on these types sets min_required: 1. Validate via record.validate_relationship_cardinality!.
- Resource DSL - Attributes, relationships, identities, actions, policies, calculations
- Type System - Built on dry-types with Ash-style names (
:atom,:uuid, etc.) - Changeset - For tracking changes and validations
- Query Builder - Filtering (with OR/AND compound conditions), sorting, pagination, immutable API
- Result Monad - Railway-oriented programming with pattern matching support
- Preloading - Efficient batch-loading of relationships to avoid N+1
- Calculations & Aggregates - Derived fields computed on-demand (count, sum, avg, min, max, etc.)
- Memory - In-memory storage for testing (thread-safe)
- Sequel - PostgreSQL and SQLite support (fully wired to resources)
- YAML Frontmatter - File-based persistence with YAML frontmatter + Markdown body
- JSONL - JSON Lines append-only log with optional hash-chaining (ideal for event stores)
See ADR-001: Store Composition for unified multi-store architecture.
- Schema ID & Version -
schema_id "my-resource",schema_version "2.0.0" - Attribute Lifecycle -
since:,deprecated:,removed:options - Upcasting - Transform old data to current schema on read
- Compatibility Declarations -
backward_compatible_with,forward_compatible_with - UUID7 & UUID8 - Time-sortable UUIDs (RFC 9562) for natural ordering
- Tool Definition Export -
Resource.to_tool_definitionsgenerates Anthropic/OpenAI tool schemas - Actions become typed function calls with proper JSON Schema parameters
- Constraints, descriptions, and required fields all preserved
- Snapshots - Captures resource definitions as JSON
- Differ - Compares snapshots to detect changes
- Codegen - Generates Sequel migrations from diffs
- CLI -
archema codegen,archema migrate, etc.
Key features:
- Resources are authoritative, migrations are derived
- UUIDv7 for PostgreSQL (time-ordered, better indexing)
- Dialect-aware (PostgreSQL vs SQLite differences handled)
- Interactive conflict resolution for renames/type changes
Understanding how Archema operations flow helps debug issues and design correct systems.
All CRUD operations follow a consistent pipeline:
Write Actions (create/update/destroy):
- Action lookup — Verify action exists; fail with
InvalidActionErrorif not - Atomicity check — If
require_atomic: true, verify adapter supports transactions - Authorization — Check policies; fail with
AuthorizationErrorif denied - Build changeset — Separate params into attributes vs arguments, enforce write protections (
:readonly,:immutable) - Validate changeset — Type coercion, required fields, constraints, composition rules
- Apply changes — Run action's transformations (
set_timestamp, etc.) - Execute via adapter — Wrapped in transaction if
require_atomic?
Read Actions:
- Action lookup — Verify action exists
- Authorization — Check policies
- Apply preparations — Tenant scoping, policy filters, custom preps
- Execute query — Send to adapter with filters, sorts, pagination
- Preload relationships — Batch-load via
load:option - Load calculations — Compute aggregates via
load_calculations:
Context Propagation: Actor and tenant flow through the entire pipeline, including preloaded relationships:
User.read(actor: admin, tenant: org, load: :posts)
# → Posts are also read with actor: admin, tenant: org| Operator | SQL Equivalent | Example |
|---|---|---|
:eq |
= |
where(:status, :eq, :active) |
:neq |
!= |
where(:status, :neq, :deleted) |
:gt / :gte |
> / >= |
where(:age, :gte, 21) |
:lt / :lte |
< / <= |
where(:price, :lt, 100) |
:in / :not_in |
IN / NOT IN |
where(:role, :in, [:admin, :mod]) |
:like / :ilike |
LIKE / ILIKE |
where(:email, :ilike, "%@gmail%") |
:contains |
LIKE %v% |
where(:bio, :contains, "ruby") |
:starts_with |
LIKE v% |
where(:name, :starts_with, "Dr.") |
:ends_with |
LIKE %v |
where(:email, :ends_with, ".edu") |
:is_nil / :is_not_nil |
IS NULL |
where(:deleted_at, :is_nil, true) |
:matches |
~ (regex) |
PostgreSQL only |
Query Composition: Conditions combine with AND at top level; use where_any for OR:
query.where(active: true) # active AND
.where_any([{role: :admin}, {role: :mod}]) # (admin OR mod)Note:
filterandfilter_anyare aliases for Ash Framework users.
Policies are declarative and composable:
| Check Result | Meaning |
|---|---|
:authorized |
Policy permits; continue checking others |
:bypass_authorized |
Policy permits AND short-circuits (skip remaining) |
:forbidden |
Policy denies; authorization fails immediately |
:continue |
No decision; treated as permitted |
Evaluation rules:
- If no policies apply, authorization succeeds (open by default)
- All applicable policies must authorize (AND logic)
- Bypass policies can short-circuit evaluation
policies do
# Admin bypass - short-circuits remaining checks
bypass actor_attribute_equals(:admin, true) do
authorize_if always
end
# Normal users: must own the record
policy action_type(:update) do
authorize_if relates_to_actor_via(:author)
end
endWhen a Resource has multiple stores (primary, cache, projection, event):
| Operation | Routing Order | Behavior |
|---|---|---|
| read | cache → projection → primary | First non-empty result wins |
| get | cache → projection → primary | First found record wins |
| count | projection → primary | Projection preferred (SQL COUNT) |
| create | primary → event → projections | Must succeed on primary |
| update | primary → event → projections | Must succeed on primary |
| destroy | primary → event → projections | Cache invalidated |
Atomicity Warning: Multi-store writes are NOT atomic across heterogeneous stores. If primary succeeds but event/projection fails, the system may be inconsistent. Use event store as source of truth and rebuild_projection! for recovery.
| Adapter | Transactions | Isolation | Nested |
|---|---|---|---|
| Sequel | Full ACID | DB default (READ COMMITTED / SERIALIZABLE) | Savepoints |
| Memory | None | N/A | N/A |
| JSONL | None (append-only) | N/A | N/A |
| YAML | None (file-based) | N/A | N/A |
For atomic multi-record operations, use Archema.batch:
Archema.batch do |b|
user = b.create(User, name: "Alice")
b.create(Post, author_id: user.id, title: "Hello")
end # Wrapped in single transaction if adapter supports itThe differ detects changes between snapshots:
| Scenario | Detection | Resolution |
|---|---|---|
was: hint in DSL |
Explicit rename | Auto-generates RenameColumn |
| 1 removed + 1 added, same type | Possible rename | User chooses :rename or :separate |
| Type changed | Conflict | User must choose :alter or :abort |
Expand/Contract Pattern for safe renames:
- Expand: Add new column, deploy code writing both
- Contract: Remove old column after all code uses new
archema codegen [NAME] # Generate migrations from resource changes
archema migrate # Run pending migrations
archema rollback # Rollback last migration
archema snapshot # Show current resource snapshots
archema help [COMMAND] # Show help for any commandArchema is currently distributed via local path reference for internal use.
Add to your project's Gemfile:
gem "archema", path: "~/src/archema"Then:
bundle install
bundle binstubs archema # Optional: creates bin/archema for CLI accessbin/dx gem # Build the gem
bin/dx gem install # Build and install locally
bin/dx gem clean # Remove built gem filesAll development tasks use bin/dx (a toys-core based CLI):
bin/dx test # Unit tests
bin/dx test integration # SQLite/PostgreSQL integration tests
bin/dx test all # Everything
bin/dx test -f path/to/test.rb # Run specific file
bin/dx test -n test_name # Run test by name pattern
# Property-based testing
bin/dx prop # All property tests
bin/dx prop run query # Single module
bin/dx prop list # List available modules
# Mutation testing
bin/dx mutant # Run on all covered code
bin/dx mutant Archema::Query # Run on specific subject
# Simulation-driven testing (LLM-generated scenarios)
bin/dx sim generate # Generate scenario using Sonnet/Haiku
bin/dx sim run # Run pending scenarios
bin/dx sim # Library summary
# Type checking
bin/dx types # Run Steep type checker
bin/dx types extract # Extract RBS from inline annotations
# Linting and pre-commit
bin/dx lint # Run RuboCop
bin/dx lint fix # Auto-correct offenses
bin/dx pre-commit # Run all checks (lint, types, tests, etc.)See KEY_FILES.md for complete documentation and source file reference.