Organizing Your Project

Start with one file per definition. Add subdirectories when a business domain or process becomes large enough to need them. The examples below are suggestions, not required layouts. See Project structure for the discovery rules shared by every primitive.

Workflows

Grouping workflows by business process keeps their orchestration and steps easy to navigate. A simple workflow can live in one file; a larger one can have its own directory. The main file composes the steps, and execution order is defined in the workflow, so filenames do not need numbering. Actions referenced by a discovered workflow stay in actions/ for action discovery.

Example:

TXT
workflows/
├── sendPaymentReceipt.ts
└── invoice-reminder/
    ├── invoiceReminder.ts
    └── steps/
        ├── loadInvoiceContext.ts
        └── composeReminder.ts

Actions

Grouping actions by business object helps readers find what can be done with that object. A subdirectory can group operations on a related object when that relationship is useful for navigation.

Names such as markInvoicePaid or sendReminder describe the business intent. The available actions follow the operations the application needs; a full set of CRUD actions for every object is not necessary.

Example:

TXT
actions/
└── invoice/
    ├── markInvoicePaid.ts
    ├── sendReminder.ts
    └── invoice-line/
        └── applyDiscount.ts

Connectors

Grouping connectors by external system makes integration code easy to find. An additional product level can help when one provider exposes several products. A custom connector can have its own directory when its definition, client, and types benefit from separate files.

Example:

TXT
connectors/
├── payment-gateway.ts
├── acme/
│   ├── accounting.ts
│   └── crm.ts
└── legacy-erp/
    ├── legacy-erp.ts
    ├── client.ts
    └── types.ts

Datasets

For ingested data, grouping datasets by source makes their origin visible. For derived data, a business domain often gives a more useful grouping. A dataset file describes a table's contract; retrieval and transformation live in syncs and pipelines.

Example:

TXT
datasets/
├── legacy-erp/
│   ├── customers.ts
│   └── invoices.ts
└── billing/
    └── overdue-invoices.ts

Syncs

Matching sync paths to the datasets they populate makes it easier to move between a table's contract and its ingestion code. Reading and mapping can stay in the sync file until their complexity makes separate helpers useful.

Example:

TXT
syncs/
└── legacy-erp/
    ├── customers.ts
    └── invoices/
        ├── invoices.ts
        ├── read-invoices.ts
        └── map-invoice-row.ts

Pipelines

Source-based grouping works well for provider-specific transformations; domain-based grouping works well for business processing. A short pipeline can keep its steps and SQL in one file. Larger queries can be easier to maintain as separate SQL files, with a sql.ts helper to load or compose them when needed.

Example:

TXT
pipelines/
├── normalize-customers.ts
└── billing/
    └── overdue-invoices/
        ├── overdue-invoices.ts
        ├── sql/
        │   ├── unpaid-invoices.sql
        │   └── payment-totals.sql
        └── sql.ts

Ontology

One file per object type keeps its properties, links, and telemetry declarations together. Domain directories can help as the ontology grows. A value-types/ directory gives shared value types a recognizable home; a value type used by one object can stay with that object.

Example:

TXT
ontology/
├── customer.ts
├── billing/
│   ├── invoice.ts
│   └── invoice-line.ts
└── value-types/
    └── currency-code.ts

Projections

Following the ontology's organization makes projections easy to find from their target business object. Foreign-key links can stay within the object projection using .withLinks(...). Dedicated files help distinguish separate link or telemetry projections from the projection that materializes the object itself.

Example:

TXT
projections/
├── customer.ts
└── billing/
    ├── invoice.ts
    ├── invoice-line.ts
    ├── invoice-reviewers.ts
    └── invoice-balance-telemetry.ts

Security

Group security files by team or business domain. Groups collect users and service accounts, roles grant access, and membership policies control who can administer a group.

Example:

TXT
security/
├── groups/
│   └── billing-team.ts
├── roles/
│   └── billing-access.ts
└── policies/
    └── billing-membership.ts

See Authorization for how the definitions work together.

App

Grouping UI code by user-facing feature keeps components and data access close to the screens that use them. In this layout, each page.tsx composes features for its route, and _features/ holds the feature implementations. The name _features is a choice: the _ prefix is what excludes the directory from routing.

A small feature can start with one component. Separate components/, utils/, hooks/, queries.ts, or mutations.ts become useful when there is enough code to group. A sub-feature can use the same approach as it grows.

Example:

TXT
app/
├── layout.tsx
├── invoices/
│   └── page.tsx
└── _features/
    └── billing/
        ├── components/
        │   └── InvoiceTable.tsx
        ├── queries.ts
        ├── mutations.ts
        └── payment-history/
            ├── components/
            │   └── PaymentHistory.tsx
            └── queries.ts

See Building Apps for routing, styles, and the available UI packages.

Search docs

Search the documentation