This guide helps contributors decide where to build features, what packages own which responsibilities, and how to handle cross-package changes.
- Avoid accidental boundary crossing between
client,sdk,backend,indexer, andoracle. - Keep feature work in the package that owns the domain.
- Make issue triage and reviewer selection easier.
- Consumer web application and user interface.
- Pages, navigation, charts, forms, wallet UX, client-side validation, and browser state.
- Uses
@tikka/sdkfor contract interactions and calls backend APIs for metadata, user state, and notifications. - Typical touchpoints:
client/src/client/public/client/package.jsonclient/playwright.config.ts
- Client-facing contract library and transaction building layer.
- Builds, simulates, signs, and submits Soroban transactions.
- Encapsulates contract bindings, wallet adapter abstractions, network config, and reusable protocol types.
- Used by
clientand other integrators. - Typical touchpoints:
sdk/src/sdk/tests/sdk/package.json
- API layer, authentication, metadata, notifications, and business logic.
- Hosts endpoints that support client flows and enrich data with backend-managed state.
- Responsible for server-side validation, email/push notifications, and auth refresh.
- Typical touchpoints:
backend/src/backend/test/backend/package.jsonbackend/README.md
- Blockchain event ingestion and query layer.
- Reads Horizon events, decodes logs, updates PostgreSQL, and maintains Redis caches.
- Owns history tables, event-driven join data, and read-optimized query support.
- Typical touchpoints:
indexer/src/indexer/test/indexer/redis.confindexer/package.json
- Randomness oracle workflow and contract callback orchestration.
- Watches for draw requests, computes/verifies randomness, and submits the result to the contract.
- Owns PRNG/VRF logic and the contract relay path.
- Typical touchpoints:
oracle/src/oracle/test/oracle/package.json
- Repository documentation, onboarding guides, architecture references, and contributor guidance.
- Owns public-facing docs and internal guidance that help new contributors understand boundaries.
- Typical touchpoints:
docs/- root
README.md - package-level
CONTRIBUTING.md
- Deployment, infrastructure, environment configuration, and CI/CD support.
- Owns repository-level automation, environment variables, monitoring, and release workflows.
- Typical touchpoints:
.github/docker-compose.yml- root scripts and package install/build orchestration
- repo-level config files
- Build in:
client - Common touched directories:
client/src/client/public/client/package.jsonsdk/only when a new contract interaction or wallet adapter is required
- Build in:
sdk - Common touched directories:
sdk/src/sdk/tests/client/src/for UI integrationbackend/src/only when a new backend API is required for metadata or auth
- Build in:
backend - Common touched directories:
backend/src/backend/test/client/src/for UI displayindexer/only when new historical data is required
- Build in:
indexer - Common touched directories:
indexer/src/indexer/test/backend/src/for API access to the indexed dataclient/src/for showing analytics
- Build in:
oracle - Common touched directories:
oracle/src/oracle/test/backend/src/when backend state or status endpoints must reflect oracle eventsindexer/src/when new events need to be indexed
- Build in:
ops - Common touched directories:
.github/- root-level scripts and config files
- package-level
Dockerfile/docker-compose.yml
- Build in:
docs - Common touched directories:
docs/- root
README.md - package-level
CONTRIBUTING.md
When a change touches more than one package, verify the following:
- Is there a strong domain reason to modify multiple packages in one PR?
- Does the change preserve package ownership and responsibility boundaries?
- Were package-level tests and build checks run for each affected package?
- Is the change surfaced clearly in issue descriptions and PR titles?
- Have maintainers or reviewers for all touched packages been requested?
- Are non-functional changes (style, formatting, lint) scoped to the package they belong to?
- Were shared interfaces, DTOs, and contract bindings updated in only one source-of-truth package?
- If the change includes docs or onboarding, did you link to
docs/contributing/MODULE_BOUNDARIES.md?
- Prefer the owning package for any feature work.
- Keep
clientfocused on the UI and user experience. - Keep
sdkfocused on contract integration and transaction plumbing. - Keep
backendfocused on API logic, state, metadata, and notification behavior. - Keep
indexerfocused on blockchain ingestion and query model design. - Keep
oraclefocused on randomness delivery and contract callback workflow. - Keep
docsfocused on onboarding, architecture, and written guidance. - Keep
opsfocused on deployment, CI/CD, and repository automation.
If you are unsure where a feature belongs, open an issue and tag package maintainers. Cross-package work is allowed, but it should be deliberate and reviewed by all affected teams.
The backend reads indexer-sourced data only through the anti-corruption layer in
backend/src/services/indexer/. This directory owns all indexer integration
in the backend: the HTTP client, the backfill/replay pipeline, and the types on
each side of the boundary.
- Raw indexer wire shapes live in
backend/src/services/indexer/indexer-api.types.tsand mirror what the indexer actually returns over HTTP (seeindexer/src/api/controllers/and its DTOs). These are the indexer's row shapes — the schema the indexer owns. - Backend-owned response types live in
backend/src/services/indexer/indexer.types.ts. These are the stable shapesIndexerServicereturns and that controllers — and through them the client — consume. This is the API contract the backend owns. - Translation happens once, in
backend/src/services/indexer/indexer.mapper.ts, and is pinned bybackend/src/services/indexer/indexer.mapper.spec.ts. A change to an indexer row shape must be absorbed in the mapper; if the mapping breaks, a backend test fails instead of an API consumer seeing a silently changed response.
IndexerService is the only module that may import the raw indexer wire shapes.
This is enforced automatically by dependency-cruiser
(backend/.dependency-cruiser.js, run via pnpm run boundaries in backend/ and
in CI):
- Rule
no-indexer-row-shapes-outside-boundary: importingsrc/services/indexer/indexer-api.typesfrom anywhere outsidesrc/services/indexer/fails the check. - Consumers outside the boundary must import the backend-owned response types
from
src/services/indexer/indexer.types(or its re-exports viaindexer.service), neverindexer-api.types.
- Add the wire shape to
indexer-api.types.ts(from the indexer's controller/DTO). - Add the backend-owned response type to
indexer.types.ts. - Add a mapper function in
indexer.mapper.tsand a test inindexer.mapper.spec.ts. - Expose the method on
IndexerService, returning only backend-owned types.
Module-boundary checks run automatically:
- Backend:
pnpm run boundaries(dependency-cruiser) in the backend CI job.
If a PR introduces a forbidden cross-boundary import, CI fails with a message pointing at this document.