Files
sure/docs/llm-guides/architecture.md
T
Juan José Mata 157daf5176 Consolidate repository instructions after auditing their history (#3409)
* Document instruction inventory and preservation decisions

Trace main history from September 2025 through September 2026, including earlier policy origins. Record preserved requirements, detailed-guide destinations, stale facts, harness boundaries and explicit policy-strength decisions before consolidating instruction sources.

* Consolidate repository instructions into shared guidance

Keep AGENTS concise and vendor neutral, move detailed conventions into shared guides, and use thin adapters with preserved Cursor scopes. Preserve the strict pre-PR checks globally and document the stronger scope, retired migration pin and rule-generation trigger. Update existing API guidance verification without changing application behavior.

* Narrow the always-on Cursor UI adapter and correct the SimpleFIN comment

Split the design-system guidance out of docs/llm-guides/ui.md into
docs/llm-guides/design-system.md. The ui-ux-design-guidelines rule is
alwaysApply: true, so importing all of ui.md loaded the Stimulus,
localization and ViewComponent guidance (previously confined to scoped
rules) on every Cursor session; the always-on adapter now imports only the
design-system guide, matching the scope it had before the consolidation.
view_conventions and stimulus_conventions keep the full UI guide.

Also correct the stale Provider::Simplefin header comment: pending
inclusion defaults on and is resolved by the importer (explicit argument,
then SIMPLEFIN_INCLUDE_PENDING, then Setting.syncs_include_pending); the
previous comment described the flag as default-off.

* Read guidance files as UTF-8 in the API consistency validators

The frontmatter regex match ran against content read with the locale
default external encoding; the Cursor rule's description contains an em
dash, so under US-ASCII (LC_ALL=C) Regexp#match raised ArgumentError,
breaking the standalone no-Rails fallback the docs point contributors to.
Read all checked files with an explicit UTF-8 encoding in both the
standalone script and the Rails test.
2026-09-06 07:14:43 +02:00

8.6 KiB

Architecture and development conventions

Read the relevant models and schema before changing domain behavior. The application supports managed and self_hosted modes through Rails.application.config.app_mode; see application configuration. Provider availability can differ between installations.

Design conventions

  • Push built-in Rails functionality before adding dependencies. A new dependency needs a strong technical or business reason; favor established, reliable tools.
  • Keep controllers thin and business logic in app/models/, organized with POROs and concerns rather than introducing service-object directories. A concern may serve one model, but organize it around a model trait rather than merely moving code elsewhere. Prefer account.balance_series to AccountSeries.new(account).call.
  • Optimize for clear object design and readability. Focus performance work on critical paths and shared surfaces: avoid N+1 queries and loading large payloads in global layouts; use indexes, eager loading, background jobs and caching where they address a real cost.
  • Put simple constraints such as null checks and uniqueness in the database. ActiveRecord may mirror them for form error handling; prefer client-side form validation where possible. Keep complex validation and business logic in Ruby.
  • Use semantic HTML and Hotwire, server-side formatting and URL state. Detailed component, Turbo, Stimulus and design-system rules are in UI guidance.

Families, users and currencies

Family owns financial accounts, users, subscriptions and many preferences. User belongs to a family; Session belongs to a user. Roles include guest, member, admin and super admin. Accounts also have an optional user owner and sharing rules, so family membership alone does not describe every user's access. Use Current.user and Current.family, and the existing account-access scopes for the surface being changed.

An Account has its own balance and currency. Entries, balances and holdings also retain currencies. The family's preferred currency is used to normalize reports; it does not mean every stored amount is already in that currency. Money handles monetary operations and formatting, with ExchangeRate and its Provided concern supplying dated conversion rates.

Accounts, balances and entries

Account uses a delegated accountable type. The supported types are defined in Accountable: asset types Depository, Investment, Crypto, Property, Vehicle, OtherAsset; liability types CreditCard, Loan, OtherLiability.

A daily balance records what an asset is worth or what is owed on a liability. For a depository account this is cash; for an investment account it includes cash and holdings value. A holding records an account's quantity and price of a Security on a date. Balance::Materializer materializes holdings through Holding::Materializer before calculating balance history.

Entry delegates to one of the three Entryable types, with a date, amount and currency:

  • Valuation is an absolute account value or debt at a date, not an income or expense.
  • Transaction changes the account balance and can have a category, merchant and tags; rules can enrich or classify it.
  • Trade represents a security movement with quantity and price, including buys and sells.

For cash movements, negative entry amounts are inflows; positive amounts are outflows. A negative checking transaction increases cash; a negative credit-card transaction is a payment that reduces debt. A sale's negative entry amount increases investment-account cash. Do not apply movement signs to an absolute valuation or confuse cash direction with asset/liability balance direction.

Transfers

Transfer pairs an inflow and outflow transaction between different accounts in the same family. Same-currency amounts must be equal and opposite. Family::AutoTransferMatchable normally searches a four-day window and also supports cross-currency candidates using dated exchange rates and a tolerance. Confirmed transfers permit a thirty-day date difference; do not assume every transfer is a same-currency, four-day match.

The destination determines the outflow's kind: loan payment, credit-card payment, investment contribution or ordinary funds movement. In Transaction's budget classification, funds movement and credit-card payments are excluded, while loan payments and investment contributions count as expenses. Preserve these distinctions rather than treating every transfer as excluded from income/expense reporting.

Ingestion and background work

Provider connections such as PlaidItem hold connection metadata and provider account payloads; processors normalize them into internal accounts and entries through Account::ProviderImportAdapter. AccountProvider connects accounts to provider records. Import supports manual import sessions, including CSV mapping and transformations. Plaid is one of many supported provider integrations.

Syncable schedules background syncs and Sync records their state, hierarchy and errors. Account::Syncer imports market data, materializes history (reverse for linked accounts, forward for manual accounts), applies provider balance overrides and matches transfers after sync. Family::Syncer schedules all eligible Syncable *_items associations plus manual accounts, then matches transfers and applies active rules. Entry-changing workflows call Entry#sync_account_later; inspect the calling workflow rather than assuming every save schedules a full sync.

AutoSync can request a family sync on login once per date when the family enables it and has active accounts. AutoSyncScheduler and the Sidekiq schedule handle scheduled work. Sidekiq also runs SyncJob, ImportJob and AssistantResponseJob.

Provider interfaces and APIs

Interchangeable provider concepts are registered at runtime through Provider::Registry and Setting, with environment overrides where supported. Interfaces live in app/models/provider/*_concept.rb, including SecurityConcept and ExchangeRateConcept. One-off integrations can expose concrete methods without inventing a shared concept. Domain models should normally select providers through their Provided concerns rather than calling the registry throughout business logic.

Concept providers inherit from Provider and use with_provider_response to return Provider::Response (success?, data, error). Raise when valid data cannot be produced inside that wrapper; it converts failures into the response contract. See provider guidance and adding a securities provider for details.

Web interaction uses Turbo/Stimulus with server-rendered views. External /api/v1 endpoints support separate Doorkeeper OAuth and X-Api-Key authentication; API keys are opaque keys, not JWTs. The API base controller sets current context, skips session CSRF checks and applies API-key rate limits. Preserve the existing authentication, scope and strong-parameter behavior. Follow the API consistency guide for behavioral tests and documentation-only rswag specs; its API-key convention does not remove runtime OAuth support.