Bootstrap dft init for one-command serve in dbt repos
Problem
Serving a real dbt analytics repo currently requires long commands with explicit face paths, project-dir, and adapter environment setup. After initialization, users should be able to run dft serve from repo root with sensible defaults.
This task should define:
1. What dft init should scaffold vs what should be autodetected.
2. How to safely detect dbt project and faces locations and persist defaults.
3. How to ensure BigQuery/dbt behavior respects dbt target + generate_schema_name.
4. Migration guidance for existing repos like Fivetran/analytics.
Required guardrails for this task:
1. Keep faces/ as the default face root for now (no required custom config for basic usage).
2. Make dft serve follow dbt conventions by default for profile + target resolution, rather than introducing a parallel warehouse config path.
3. Treat dbt as the source of truth for adapter/auth/project context; dft-specific config should focus on serving/runtime behavior only.
Context
What already exists
dft init(dataface/cli/commands/init.py,dataface/cli/main.py) — Createsfaces/,faces/partials/, starterfaces/hello.yml. Idempotent; nodataface.ymlyet. Related completed planning:tasks/workstreams/dashboard-factory/tasks/add-dft-init-for-dbt-native-repo-bootstrap.md.dft serve(dataface/cli/commands/serve.py,dataface/core/serve/server.py) —create_server(..., project_dir=None)already defaultsproject_dirtoPath.cwd(), so from repo root, URLs like/faces/hello/resolve to./faces/hello.ymlif the user runsdft servefrom the dbt project root.- Render context discovery (
dataface/core/project_roots.py) — Walks up from the face file to finddataface.yml/_sources.yaml/dbt_project.ymlfor compile/execute context. - Execution — When
dbt_project.ymlis found,DbtAdapteris registered (dataface/core/serve/server.py). It resolves profile fromdbt_project.ymland usestarget_namedefaultdevunless constructor args override (dataface/core/execute/adapters/dbt_adapter.py).SqlAdapteris still registered with CLIdialectdefaultduckdbfor non-dbt query paths.
Gaps this task closes
- No persisted “repo anchor” — Users still rely on remembering cwd; monorepos or running from subdirs break without
--project-dir. A minimaldataface.yml(or agreed marker file) can anchor discovery and optional serve defaults without duplicating warehouse secrets. dialect=duckdbdefault — Harmless for pureDbtAdapterpaths but wrong for anything that falls through toSqlAdapterwithprofile_type=dialectin a BigQuery/Snowflake repo unless the user passes flags every time.- dbt target / schema behavior — Analysts expect
DBT_TARGET,profiles.yml, and macros likegenerate_schema_nameto matchdbt run/dbt compile. Today serve does not document or consistently thread target override intoDbtAdapter(CLI has no--target). dft initoutput — Next steps saydft servebut do not create config that encodes “this repo is dbt-native; prefer dbt execution” or optional non-secret defaults.
Files likely touched (implementation phase)
| Area | Path |
|---|---|
| Init scaffold | dataface/cli/commands/init.py |
| Serve CLI + server wiring | dataface/cli/commands/serve.py, dataface/cli/main.py, dataface/core/serve/server.py |
| Config load / merge | dataface/core/compile/config.py (or new small module if cleaner) |
| dbt adapter wiring | dataface/core/execute/adapters/dbt_adapter.py (target/profile from env + optional config) |
| Docs | docs/docs/guides/installation.md, docs/docs/guides/cli-reference.md |
| Tests | tests/ mirrors above; fixtures under tmp_path dbt layouts |
Constraints (from problem statement)
- Default face root remains
faces/for basic usage. - No parallel warehouse config — credentials and database identity stay in dbt
profiles.yml/ env;dataface.ymlonly holds Dataface/runtime/serving preferences. - dbt is source of truth for adapter/auth/project context; Dataface config complements, not replaces.
Possible Solutions
Option A — Docs only
- Document: always
cdto dbt root, rundft serve, pass--dialect bigquery(and--connectionwhen needed). - Pros: No code. Cons: Does not meet “one command”; repeats friction; easy to get wrong.
Option B — dft init writes dataface.yml; dft serve reads only that file
- Pros: Explicit anchor. Cons: Temptation to put warehouse settings in YAML; duplicates dbt; conflicts with guardrails unless schema is tightly limited.
Option C — Hybrid: minimal dataface.yml + dbt-first serve defaults (Recommended)
dft initwrites a smalldataface.ymlwhen missing (never overwrite without--force), e.g. commented template with:serveor top-level keys allowed only for: default face root (faces— default), optional dbt target hint (non-secret string), optional flags that map to existing dbt env conventions (DBT_TARGET,DBT_PROFILES_DIR) in docs rather than duplicating secrets.dft serveresolution order (design to implement): 1. Explicit--project-dirwins. 2. Else walk up from cwd fordataface.yml/dbt_project.ymlto set repo root (same spirit asdiscover_render_context). 3. Dialect forSqlAdapter: if a dbt project is found, infer adapter type from resolved dbt profile target (read-only introspection) or from an allowed key indataface.ymlonly if it cannot be inferred — avoid requiring users to pass--dialectfor standard dbt repos. 4.DbtAdaptertarget: honorDBT_TARGET/dbt_project’s target when consistent with dbt CLI; optional--targetondft serveas escape hatch.- Pros: Matches guardrails; one-command serve from root; monorepo-friendly; escape hatches stay explicit. Cons: Requires careful config schema + tests so
dataface.ymldoes not become a second profiles file.
Plan
Phase 0 — Design spec (before coding)
- Write a short RFC or section in this task listing allowed
dataface.ymlkeys for M1 (serve + paths only). Explicitly forbidden in that list: connection strings, passwords, service account JSON paths (preferGOOGLE_APPLICATION_CREDENTIALS/ dbt’s own mechanisms). - Define resolution algorithm for “repo root” when both
dbt_project.ymlanddataface.ymlexist in different directories (edge case:faces/only under package subfolder). Default rule: prefer directory containingdbt_project.ymlas serve root;dataface.ymlbeside it is canonical. - Define behavior when cwd is not repo root — error message vs walk-up vs require
--project-dir.
Phase 1 — dft init alignment
- Extend
init_commandto createdataface.ymlwhen absent (idempotent; skip if exists unless--forcewith clear policy on merge vs overwrite). - Template content: version comment,
facesroot default, pointer to docs forDBT_TARGET/ profiles, optional commentedserve:block if needed. - Update
dft inittests: assert file created; re-run no-op;--forcebehavior.
Phase 2 — dft serve defaults
- Implement project root discovery from cwd (reuse or extend
project_rootshelpers to avoid drift with compile paths). - Wire discovered root into
create_serverwhen--project-diromitted. - Infer
SqlAdapterdialect from dbt profile type when dbt project present; keep--dialectas override. - Pass dbt target into
DbtAdapterfromDBT_TARGETenv and optional--target; document interaction withdbt_project.ymldefault target.
Phase 3 — BigQuery / generate_schema_name
- Verify (add integration or smoke test if feasible) that queries executed via
DbtAdapteruse the same adapter instance / schema logic as dbt forref()/source()(macro resolution path). If a gap exists, file a sub-issue or fix in the same PR with a regression test. - Document that raw SQL in faces must respect compiled schema names (same as dbt); link to dbt docs on custom schemas.
Phase 4 — Migration (e.g. Fivetran/analytics)
- Runbook:
dft initin repo root → review generateddataface.yml→ setDBT_TARGETas needed →dft servefrom root. - Note any analytics-repo-specific paths (if dashboards live outside default
faces/) and whether--project-diror a config key is required — prefer one blessed layout for M1.
Phase 5 — Docs and CLI help
- Update installation / CLI reference: “after init, from repo root:
dft serve”. - Document env vars:
DBT_TARGET,DBT_PROFILES_DIR, and when--connectionis still needed (non-dbt SQL paths, demos).
Acceptance criteria
- [ ] From a minimal dbt repo fixture (with
dbt_project.yml+faces/hello.yml),dft servewith no flags (run from root) serves/faces/hello/without requiring--project-diror--dialectfor typical dbt-backed queries. - [ ]
dft initcreatesdataface.ymlonce; safe re-run. - [ ]
DBT_TARGET(or--target) changes which warehouse targetDbtAdapteruses, consistent with dbt expectations. - [ ] Docs include migration steps for an existing repo; no recommendation to duplicate warehouse config in
dataface.yml.
Out of scope (unless explicitly pulled in)
- Replacing dbt profiles with Dataface-native connection config.
- Changing default face root away from
faces/for M1. - Cloud/playground product surfaces (CLI + local serve only unless shared code is trivial).
Implementation Progress
Phase 1 — dft init creates dataface.yml
- Added
DATAFACE_YMLtemplate todataface/cli/commands/init.py— comments-only YAML that documentsserve:keys (dialect,target) and anchors the project root. - Added
"dataface.yml"as the first scaffold entry so it's created beforefaces/. Idempotent: skips if exists;--forceoverwrites.
Phase 2 — dft serve defaults (project root discovery + dialect inference + target)
Project root discovery (dataface/core/project_roots.py):
- discover_project_root(start_dir) walks up from start_dir (default: cwd) looking for dataface.yml, dataface.yaml, or dbt_project.yml. Returns the directory containing the first match, or start_dir if none found.
- infer_dialect_from_dbt(project_dir, target_name) reads dbt_project.yml → profile name → profiles.yml target → type field. Falls back to ~/.dbt/profiles.yml. Returns None when unresolvable.
Serve CLI (dataface/cli/commands/serve.py):
- dialect default changed from "duckdb" to None. When None, auto-detects from dbt profile; falls back to "duckdb".
- Added --target parameter threaded through to DbtAdapter.
- serve_command now calls discover_project_root() when --project-dir omitted.
CLI main (dataface/cli/main.py):
- --dialect default None (was "duckdb"), --target option added.
Server (dataface/core/serve/server.py):
- create_server accepts optional target param, stored in app.state.target.
- _render_face_file and _handle_inspect_route accept and thread target to DbtAdapter(target_name=target).
DbtAdapter (dataface/core/execute/adapters/dbt_adapter.py):
- target_name resolution: explicit arg → DBT_TARGET env → "dev".
Tests (23 new, 143 total related tests passing)
tests/core/test_bootstrap_serve.py:
- TestDiscoverProjectRoot — 6 tests (dbt root, subdir walk-up, dataface.yml, both markers, nothing found, cwd default)
- TestDbtAdapterTarget — 4 tests (default dev, explicit, env var, explicit overrides env)
- TestInitDatafaceYml — 5 tests (create, skip existing, force, valid YAML, non-dbt repo)
- TestInferDialectFromDbt — 6 tests (bigquery, duckdb, specific target, no dbt project, no profiles, home profiles fallback)
- TestServeProjectRootDiscovery — 2 tests (server uses discovered root, threads target)
Acceptance criteria status
- [x]
dft servewith no flags from repo root serves faces without--project-diror--dialect - [x]
dft initcreatesdataface.ymlonce; safe re-run - [x]
DBT_TARGET(or--target) changes which targetDbtAdapteruses - [ ] Docs include migration steps (Phase 4–5, deferred to follow-up)
QA Exploration
N/A — CLI-only task, no UI/browser surfaces affected.
- [x] QA exploration completed (or N/A for non-UI tasks)
Review Feedback
- [ ] Review cleared