Baraba Tech Stack: Защо избрахме Crystal + Leptos
Технически deep-dive в архитектурата коята всеки може да пробва https://baraba.org
TL;DR
| Layer | Technology | Why |
|---|---|---|
| Backend | Crystal + Lucky | Ruby syntax, C speed, type safety |
| Frontend | Rust + Leptos | WASM, fine-grained reactivity, no JS |
| Database | PostgreSQL 16 | Reliability, JSON support, performance |
| Proxy | Caddy | Auto HTTPS, simple config |
| Container | Docker | Reproducible builds |
Backend: Crystal + Lucky Framework
Какво е Crystal?
Crystal е статично типизиран език, който изглежда като Ruby, но се компилира до native code.
# Това е валиден Crystal код
class Invoice
property amount : Float64
property vat_rate : Float64 = 0.20
def total
amount * (1 + vat_rate)
end
end
invoice = Invoice.new(amount: 1000.0)
puts invoice.total # => 1200.0
Защо не Ruby/Rails?
| Аспект | Ruby/Rails | Crystal/Lucky |
|---|---|---|
| Скорост | ~1000 req/s | ~50000 req/s |
| Памет | ~200MB | ~20MB |
| Type Safety | Runtime errors | Compile-time errors |
| Startup | 2-5 seconds | 10ms |
Lucky Framework Highlights
# Type-safe routing
class Api::Users::Show < ApiAction
get "/api/users/:user_id" do
user = UserQuery.find(user_id) # Compile error if user_id is wrong type
json(UserSerializer.new(user))
end
end
# Type-safe database queries
class UserQuery < User::BaseQuery
def active
is_active(true)
end
def admins
role("admin")
end
end
# Usage - compile error if method doesn't exist
users = UserQuery.new.active.admins
Avram ORM: The Safest ORM
# Operations with compile-time validation
class SaveUser < User::SaveOperation
permit_columns email, name
before_save do
validate_required email, name
validate_format_of email, with: /@/
validate_uniqueness_of email
end
end
# Usage
SaveUser.create(params) do |operation, user|
if user
# Success - user is User, not User?
json(user)
else
# Validation errors
json({errors: operation.errors})
end
end
Frontend: Rust + Leptos + WASM
Защо не React/Vue/Svelte?
| Framework | Bundle Size | Runtime | Memory |
|---|---|---|---|
| React | 42KB + deps | JS VM | High |
| Vue | 34KB + deps | JS VM | Medium |
| Svelte | 2KB + deps | JS VM | Low |
| Leptos | 0KB | WASM | Minimal |
Leptos компилира до WebAssembly – няма JavaScript runtime overhead.
Fine-grained Reactivity
#[component]
pub fn Counter() -> impl IntoView {
// Signal - reactive primitive
let (count, set_count) = create_signal(0);
// Derived signal - автоматично се преизчислява
let doubled = move || count.get() * 2;
view! {
// Само този <span> се update-ва при промяна
<span>{count}</span>
<span>{doubled}</span>
<button on:click=move |_| set_count.update(|n| *n += 1)>
"+1"
</button>
}
}
За разлика от React (който прави virtual DOM diff), Leptos знае точно кой DOM елемент да update-не.
WASM Performance
JavaScript:
Parse → Compile → Execute → GC pauses
WebAssembly:
Load → Execute
WASM е предварително компилиран. Няма parsing, няма JIT compilation, няма garbage collection pauses.
Rust Safety в Browser
// Това няма да компилира - Rust предотвратява data races
let data = create_signal(vec![1, 2, 3]);
spawn_local(async move {
// Compiler error: cannot move `data` because it's borrowed
let result = fetch_data().await;
data.set(result);
});
// Правилният начин:
let (data, set_data) = create_signal(vec![1, 2, 3]);
spawn_local(async move {
let result = fetch_data().await;
set_data.set(result); // OK - set_data is Clone
});
Database: PostgreSQL 16
Защо PostgreSQL?
- ACID compliance – критично за счетоводство
- JSON/JSONB – гъвкавост за settings и metadata
- Full-text search – търсене в описания
- Partitioning – за големи таблици с journal entries
- Row-level security – multi-tenant isolation
Schema Design Principles
-- Всяка таблица има audit columns
CREATE TABLE journal_entries (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Business columns
company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
entry_number INTEGER NOT NULL,
date DATE NOT NULL,
-- Indexes за performance
CONSTRAINT unique_entry_number UNIQUE (company_id, entry_number)
);
CREATE INDEX idx_journal_entries_company_date
ON journal_entries(company_id, date);
Connection Pooling
Crystal използва connection pool by default:
# config/database.cr
AppDatabase.configure do |settings|
settings.url = ENV["DATABASE_URL"]
settings.initial_pool_size = 5
settings.max_pool_size = 20
settings.checkout_timeout = 5.seconds
end
Infrastructure: Docker + Caddy
Multi-stage Docker Builds
# Build stage - голям image с build tools
FROM crystallang/crystal:1.17.0-alpine AS builder
COPY . .
RUN crystal build src/app.cr -o app --release
# Production stage - минимален image
FROM alpine:3.19
COPY --from=builder /app/app /app/app
CMD ["/app/app"]
Резултат: ~30MB production image вместо ~1GB.
Caddy: Automatic HTTPS
baraba.org {
# Caddy автоматично:
# - Получава Let's Encrypt сертификат
# - Redirect-ва HTTP → HTTPS
# - Renew-ва сертификата
@api path /api/*
handle @api {
reverse_proxy backend:3000
}
handle {
reverse_proxy frontend:80
}
}
Monitoring & Observability
Structured Logging
# Lucky използва Dexter за structured logs
Log.info { {
event: "invoice_created",
invoice_id: invoice.id,
company_id: invoice.company_id,
amount: invoice.total,
user_id: current_user.id
} }
Output (JSON):
{
"severity": "Info",
"timestamp": "2026-02-01T12:00:00Z",
"event": "invoice_created",
"invoice_id": 123,
"company_id": 1,
"amount": 1200.00,
"user_id": 5
}
Health Checks
class Api::Health::Show < ApiAction
include Api::Auth::SkipRequireAuthToken
get "/api/health" do
db_ok = begin
AppDatabase.run { |db| db.exec("SELECT 1") }
true
rescue
false
end
json({
status: db_ok ? "healthy" : "unhealthy",
database: db_ok,
version: App::VERSION,
uptime: Time.utc - App::STARTED_AT
})
end
end
Security Considerations
Authentication: JWT
class UserToken
def self.generate(user : User) : String
payload = {
"user_id" => user.id,
"email" => user.email,
"exp" => 24.hours.from_now.to_unix
}
JWT.encode(payload, secret_key, JWT::Algorithm::HS256)
end
def self.decode(token : String) : Hash?
JWT.decode(token, secret_key, JWT::Algorithm::HS256)
rescue JWT::Error
nil
end
end
Password Hashing: Bcrypt
# Lucky's Authentic shard
class SaveUser < User::SaveOperation
before_save do
if password = password_param
self.encrypted_password = Authentic.generate_encrypted_password(password)
end
end
end
SQL Injection Protection
Avram не позволява raw SQL interpolation:
# Това е безопасно - параметризирана заявка
UserQuery.new.email(user_input)
# Това дори не компилира
UserQuery.new.where("email = '#{user_input}'") # Compile error!
Lessons Learned
1. Type Safety е безценна
Хиляди потенциални runtime грешки хванати от компилатора.
2. WASM е production-ready
2.8MB binary, зарежда се за ~1 секунда, работи перфектно.
3. Crystal е скритият gem
По-малко известен от Go/Rust, но перфектен за web apps.
4. Docker multi-stage builds
Намалихме image size от 1.2GB на 30MB.
5. PostgreSQL е достатъчен
Няма нужда от Redis/MongoDB/Elasticsearch за 90% от use cases.
Въпроси? Пишете ни на tech@baraba.org