Layer boundaries
The first decision on a blank project is how to organise the layers. Each layer needs a clear job — and models should only know about the layer below them. Without that constraint, models start doing too much and the project becomes hard to reason about as it grows.
Schema isolation: one macro, three environments
The most useful thing in the repo is a single Jinja macro that controls schema naming across all three environments. In development, each engineer gets their own isolated namespace — nobody steps on each other's work. In CI, schemas are scoped to the PR number. In production, schemas are clean with no prefix.
dev_tanay_staging
dev_tanay_marts
pr_123_staging
pr_123_marts
staging
marts
The useful thing about this: A new engineer doesn't configure anything — they run dbt against their dev target and their schemas appear namespaced correctly. CI gets its own isolated space per PR, which means concurrent CI runs never conflict. And the cleanup macro drops everything scoped to a PR number after the run finishes, keeping the CI database clean.
A CI pipeline that only runs what changed
Running everything on every PR is what makes CI slow enough that engineers stop waiting for it. The first thing the pipeline does at Lyrebird is check whether any dbt files actually changed. If not, every downstream job is skipped — a Terraform-only PR doesn't wait for a dbt build, and the branch protection check still passes.
Branch protection requires a passing check — but if dbt didn't change, that check should still pass. A sentinel job handles this: it passes whether the build ran or was skipped. Non-dbt PRs are never blocked waiting for a check that won't run.
Deployment: merge to ship
Deployment triggers automatically on merge to main when dbt files changed. There's one non-obvious design decision: full-refresh runs are controlled by the PR title. If the title contains a specific flag, the deploy runs with full-refresh. Otherwise it's incremental. No separate workflow, no manual trigger — just a naming convention the pipeline reads before deciding how to build.
This matters because full refreshes on large incremental models are expensive and occasionally necessary. Having it controlled by the PR title means the decision is visible in the git history, reviewable in the PR, and doesn't require anyone to remember to flip a configuration somewhere.
Code style: lowercase everything
The linting config enforces a small set of rules that apply to every SQL file in the repo. The most important is the simplest: everything lowercase. Keywords, identifiers, functions, literals — all lowercase. Not because it's objectively correct, but because consistency removes a whole category of review comment. A PR that touches SQL should be reviewed for its logic, not its formatting.
What a PR has to prove
Every PR uses a shared template with the same sections in the same order: what changed, why, how it was tested, and a security check confirming no credentials were hardcoded. The template is short enough that it doesn't slow anyone down, but specific enough to catch what usually gets missed — particularly the testing evidence.
The combination of isolated schemas, diff-based CI, and a consistent PR template means review conversations focus on modelling decisions. Not formatting, not missing tests, not "did you check this against prod?" — those are handled structurally before the review starts.