CSA Admin is a multi-tenant Rails application for managing Community Supported Agriculture organizations. Each tenant has its own isolated SQLite database.
bin/ci # Full CI suite (lint, security, tests, seeds)
bin/rails test:all # Run all tests (uses "acme" tenant)
bin/rails lint:check # Check for style issues
bin/rails lint:autocorrect # Auto-fix style issuesTenant is resolved from request subdomain. Each tenant has a separate SQLite database.
Tenant.switch("acme") { } # Execute block in tenant context
Tenant.switch_each { } # Execute block for each tenant
Tenant.current # Get current tenant name
Current.org # Organization singleton (tenant settings/features)Key files: lib/tenant.rb, config/tenant.yml
Jobs inherit from ApplicationJob which includes TenantContext for automatic tenant serialization. Use TenantSwitchEachJob.perform_later("MyJobClassName") to run a job across all tenants.
Standard Rails database tasks (db:migrate, db:rollback, db:schema:load, etc.) work as expected and automatically apply to all tenant databases. Custom overrides in lib/tasks/database.rake ensure multi-tenant compatibility.
bin/rails db:migrate # Run pending migrations (all tenants)
bin/rails db:rollback # Rollback last migration (all tenants, STEP=n supported)
bin/rails db:migrate:down VERSION=xxx # Run down for a specific migration (all tenants)
bin/rails db:schema:load # Load schema into all tenant databasesKey file: lib/tasks/database.rake
Prefer Rails conventions over custom abstractions. Use models, concerns, and built-in Rails patterns first. Avoid unnecessary service objects, query objects, or form objects. Complexity should be earned, not assumed.
Put business logic in models. When a model grows:
- Extract cohesive functionality into sub-model concerns (e.g.,
app/models/member/billing.rb) - Delegate complex operations to POROs in
app/models/(e.g.,app/models/billing//invoicer.rb)
Shared concerns go in app/models/concerns/. When including multiple concerns, document callback order dependencies.
Code should speak for itself. Use clear naming and small methods instead of comments. Only add comments to explain why something non-obvious is done, never what the code does. No @param/@return yard-style docs — this is an app, not a public API gem.
Supported languages: English (en), French (fr), German (de), Italian (it), Dutch (nl).
YAML locale files use language-prefixed keys:
members:
title:
_en: Members
_fr: MembresTemplate files use language suffixes: invoice_created.en.liquid, invoice_created.fr.liquid
Two-phase process:
- During development, only add
_enand_frtranslations (if significant only) - Once finalized, add
_de,_it,_nltranslations (automatically for .yml file, on request for templates)
| Context | EN | FR | DE | NL | IT |
|---|---|---|---|---|---|
| Admin UI (buttons, hints, confirmations) | you | vous | impersonal (infinitive, passive) | impersonal | voi |
| Member-facing (member portal, emails, newsletters) | you | vous | Du (capitalized) | je/jij | tu |
| Handbook (docs for admins) | you | vous | Du (capitalized) | je/jij | tu |
German impersonal — Use infinitive constructions ("Alle Daten importieren"), passive ("Soll das wirklich durchgeführt werden?"), drop possessives ("Die IBAN" not "Ihre IBAN"). Never use "Sie" for direct address.
German Du — Capitalize Du/Dein/Dir/Dich in direct address. Adjust verb conjugations (hast, kannst, möchtest). Preserve lowercase "sie/ihre" (= they/their, 3rd person) and "Siehe" (= See).
Dutch impersonal — Same patterns as German: infinitive, passive, drop possessives. Never use "u/uw".
Dutch je — Use "je" as the default (lighter). Use "jouw" only for emphasis. Adjust verb conjugations ("Je hebt", not "U heeft"). With inversion, drop the -t ("heb je", not "hebt je").
Anonymized members must not have their member_id exposed in CSV/XLSX exports.
Convention: Use member&.display_id instead of member_id or member.id in all export code:
# ❌ Bad - leaks member_id for anonymized members
column(:member_id)
column(:member_id, &:member_id)
# ✅ Good - returns nil for anonymized members
column(:member_id) { |record| record.member&.display_id }A test in test/models/member/discardable_test.rb automatically scans export files for dangerous patterns.
Key files: app/models/member/discardable.rb, app/models/member/anonymization.rb