Baraba – счетоводен софтуер – технологичен стек

Baraba Tech Stack: Защо избрахме Crystal + Leptos

Технически deep-dive в архитектурата коята всеки може да пробва https://baraba.org


TL;DR

LayerTechnologyWhy
BackendCrystal + LuckyRuby syntax, C speed, type safety
FrontendRust + LeptosWASM, fine-grained reactivity, no JS
DatabasePostgreSQL 16Reliability, JSON support, performance
ProxyCaddyAuto HTTPS, simple config
ContainerDockerReproducible 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/RailsCrystal/Lucky
Скорост~1000 req/s~50000 req/s
Памет~200MB~20MB
Type SafetyRuntime errorsCompile-time errors
Startup2-5 seconds10ms

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?

FrameworkBundle SizeRuntimeMemory
React42KB + depsJS VMHigh
Vue34KB + depsJS VMMedium
Svelte2KB + depsJS VMLow
Leptos0KBWASMMinimal

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

Вашият коментар