Skip to content

Architecture Decision Records

AbdelrahmanWM edited this page Oct 19, 2026 · 1 revision

Architecture Decision Records (ADRs) capture the significant technical decisions behind PULSE: the context, the options considered, what was chosen and what it costs. A decision is never edited after it is accepted. If it changes, a new ADR supersedes it and the old one is marked Superseded by Dn.

Ticket: #15. Most decisions were made with Omerlo's CTO at the technical sync of 2026-10-14.

Index

# Decision Choice Status Date
D1 Hosting Fly.io app plus managed Postgres Accepted 2026-10-14
D2 Pipeline language Python Accepted 2026-10-14
D3 Search Deferred to Iteration 5 or later; then Meilisearch or Typesense rebuilt from Postgres Accepted 2026-10-14
D4 Storage Postgres plus a raw payload archive Accepted 2026-10-14
D5 Tenancy tenant_id on every table, enforced in the query layer, proven by a CI test Accepted 2026-10-14
D6 Ingestion Scheduled pull with an updated_at watermark, behind a webhook-ready interface Accepted 2026-10-14
D7 Deployment shape Standalone service that reads Omerlo through its APIs Accepted 2026-10-14

Libraries chosen under these decisions are listed in Infrastructure and Tools.


D1. Hosting

  • Status: Accepted, 2026-10-14
  • Context: Release 1 (Nov 17, 2026) must be a hosted deployment. PULSE needs one web app, one scheduled job and one Postgres database, run by a student team with little ops time. Omerlo is moving off AWS.
  • Options considered:
    • AWS (ECS or Elastic Beanstalk, RDS): the most flexible, but needs the most setup (VPC, IAM, load balancer). It is also the platform Omerlo is leaving.
    • Fly.io (app plus managed Postgres): deploys a Docker image in a few commands, has managed Postgres and built-in secrets, and is cheap at our scale.
    • Render or Railway: similar simplicity, but Omerlo did not suggest them.
  • Decision: Fly.io app plus Fly's managed Postgres.
  • Consequences:
    • Deployment stays close to our existing Dockerfiles.
    • Secrets live in Fly.io secrets.
    • Hosting cost must be confirmed with the sponsor.
    • The scheduled sync and the raw archive storage need a Fly-compatible setup, investigated in the Fly.io spike (#64).

D2. Pipeline language

  • Status: Accepted, 2026-10-14
  • Context: The pipeline (connectors, normalization, entity resolution, change detection) is the core of the project. Omerlo itself is written in Elixir.
  • Options considered:
    • Elixir: matches Omerlo's stack and is good at concurrent ingestion, but nobody on the team knows it.
    • Python: the team knows it, and the ecosystem fits the work: FastAPI, SQLAlchemy, rapidfuzz for fuzzy matching, data tooling for the evaluation.
  • Decision: Python for the backend and the pipeline (FastAPI, SQLAlchemy).
  • Consequences:
    • One language across the API and the pipeline.
    • Omerlo's team cannot reuse PULSE code directly, which is acceptable because PULSE is a standalone service (D7).

D3. Search

  • Status: Accepted, 2026-10-14
  • Context: Search and filters are a SHOULD. Release 1 volumes (hundreds to a few thousand organizations per tenant) are small.
  • Options considered:
    • Elasticsearch now: powerful, but heavy to run, and every write has to go to two stores.
    • Postgres queries only: enough for Release 1.
    • Meilisearch or Typesense later: light, typo-tolerant, simple to host.
  • Decision: No search engine before Iteration 5. When it is added, use Meilisearch or Typesense, with the index rebuilt from Postgres by a job, never dual-written by application code.
  • Consequences:
    • Release 1 lists use Postgres filtering and pagination.
    • Postgres stays the only source of truth, so the search index can always be thrown away and rebuilt.

D4. Storage

  • Status: Accepted, 2026-10-14
  • Context: PULSE must keep history, reprocess data when rules change, and trace every figure to its source.
  • Options considered:
    • Data warehouse or lakehouse (Databricks, Snowflake): built for volumes we will not have, at a cost and complexity we cannot justify.
    • Postgres only, with payloads in JSON columns: simple, but bloats the main tables.
    • Postgres plus a raw payload archive (object storage, or a local folder in development).
  • Decision: Postgres is the single source of truth. Raw payloads are archived before any transformation, keyed by tenant, sync run and entity kind, and Postgres stores a pointer and a hash.
  • Consequences:
    • Any record can be reprocessed from its raw payload.
    • Whether Omerlo allows archiving is being confirmed in #23.
    • Archive retention must be covered by the Law 25 page (#34).

D5. Tenancy

  • Status: Accepted, 2026-10-14
  • Context: PULSE will serve several Omerlo clients. One client's data must never reach another, which is also the Law 25 boundary.
  • Options considered:
    • One database per tenant: the strongest isolation, but costly to run and migrate at our stage.
    • Postgres row-level security: strong, but harder to test and debug for the team.
    • tenant_id on every table, enforced in the query layer: cheap now, and expensive to add later.
  • Decision: tenant_id on every table from the first migration, enforced by a tenant-scoped data access layer, and proven by a cross-tenant isolation test in CI (#59).
  • Consequences:
    • Every query goes through the data access layer.
    • One database per tenant stays an upgrade path if a client requires it.

D6. Ingestion

  • Status: Accepted, 2026-10-14
  • Context: Omerlo has no webhooks today and no rate limits, and results can be ordered by updated_at. Whether an "updated since" filter exists is being confirmed in #23.
  • Options considered:
    • Webhooks only: not available.
    • Full reload on every sync: simple, but wasteful as data grows.
    • Scheduled pull with a watermark: works today, and can accept webhooks later.
  • Decision:
    • Scheduled pull with an updated_at watermark per source.
    • A periodic full sync to detect deletions.
    • Every connector implements one interface (for example fetch_since(watermark) returning raw records), so file imports, other sources or webhooks plug in later.
  • Consequences:
    • Each sync run is a snapshot that change detection compares.
    • Removals are only seen by full syncs.

D7. Deployment shape

  • Status: Accepted, 2026-10-14
  • Context: PULSE may be sold as an add-on for Omerlo clients. That could mean code inside Omerlo, or a separate service.
  • Options considered:
    • Plugin inside Omerlo: tight integration, but it ties PULSE to Omerlo's codebase, stack (Elixir) and release cycle.
    • Standalone service: has its own database, authentication and UI, and reads Omerlo through its public APIs.
  • Decision: PULSE is a standalone service that reads Omerlo through its APIs. "Omerlo add-on" is how it is packaged and sold, not how the code is built.
  • Consequences:
    • PULSE can read other sources besides Omerlo.
    • It needs its own authentication and user management (#10).
    • It depends on what Omerlo's APIs expose.

Template

Copy this block for a new decision, give it the next number, and add it to the index.

## Dn. Title

- **Status:** Proposed | Accepted | Superseded by Dm, YYYY-MM-DD
- **Context:** What forces this decision: constraints, requirements, facts.
- **Options considered:**
  - **Option A:** pros and cons
  - **Option B:** pros and cons
- **Decision:** What we chose.
- **Consequences:** What becomes easier or harder, and what follow-up work it creates (link issues).

Clone this wiki locally