Skip to content

feat(appkit): generate the database operation registry and browser contracts - #690

Draft
ditadi wants to merge 1 commit into
stack/database-stable/03-operation-authorizationfrom
stack/database-stable/04-operation-generation
Draft

ditadi wants to merge 1 commit into
stack/database-stable/03-operation-authorizationfrom
stack/database-stable/04-operation-generation

Conversation

@ditadi

@ditadi ditadi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What did you change, and why?

Why: A compiled server has no TypeScript sources to scan, and the browser needs operation types without importing server code.

Change (build time only):

  • Scans server/database/routes/*.ts and writes server/database/routes.gen.ts, a lazy-import registry that satisfies DatabaseRoutes, and shared/appkit-types/database-operations.d.ts, the exact JSON contract of each operation.
  • The Vite plugin and appkit generate-types run it. With NODE_ENV=production, an operation without authorization fails generation. An unsupported Zod schema fails with the operation and the slot it came from.
  • CI type-checks the built declarations and runs a bundled server that loads its operations without sources.
  • Safe to deploy alone: an app without server/database/routes/ generates nothing new.

Interface

Generation scans the *.ts files directly in server/database/routes/ (skipping *.test.ts, *.d.ts, and *.gen.ts) and writes two files that apps commit. For the playground of the last PR of this stack:

// server/database/routes.gen.ts: the server imports this registry
import type { DatabaseRoutes } from "@databricks/appkit/beta";

export const databaseRoutes = {
  schema: () => import("../../config/database/schema"),
  modules: [
    { file: "server/database/routes/boards.ts", load: () => import("./routes/boards") },
    { file: "server/database/routes/notes.ts", load: () => import("./routes/notes") },
  ],
} satisfies DatabaseRoutes;
// shared/appkit-types/database-operations.d.ts: the browser client reads these types
declare global {
  interface DatabricksAppKitDatabaseOperations {
    "notes.update": {
      method: "PATCH";
      input: { "id": number; "body": string };
      output: { "id": number; "board_id": number; "author": string; "body": string; "created_at": string };
    };
    "notes.delete": { method: "DELETE"; input: { "id": number }; output: void };
  }
}
  • The AppKit Vite plugin runs it when the dev server starts and when files under config/database/ or server/database/routes/ change. appkit generate-types runs it from the CLI.
  • An operation without authorize is skipped with a warning naming its file#export. With NODE_ENV=production, it fails generation instead.
  • A Zod schema the client cannot type exactly fails generation with the operation and the slot: z.date(), tuples, z.json(), output transforms, coercion, and preprocessing. A failed run writes an empty interface.

How do you know it works?

Testing: generate-operations.test.ts covers discovery, validation, and strict mode. operation-contracts.types.test.ts checks that each generated contract equals the server's wire types. The shared CLI tests cover appkit generate-types. tools/verify-database-bundle.ts reports "Source-less bundle loaded 3 operations from 9 built files." The type-generator, database plugin, and shared CLI suites pass (80 files, 1520 tests). Regenerating the playground's types produced no diff, and its typecheck:database passes.


Stack created with GitHub Stacks CLI • Give Feedback 💬

…ntracts

Scan server/database/routes/*.ts and write a lazy routes.gen.ts

registry and the exact JSON contract of each operation. The Vite

plugin and appkit generate-types run it, strictly in production.

An app without that directory generates nothing new.

CI checks the built declarations and a server bundle that loads

its operations without sources.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi added this pull request to stack #693 October 23, 2026 15:01
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.3 MB (+28 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.3 MB (+21 KB) 466 KB (+8.7 KB)
Type declarations 478 KB (+14 KB) 173 KB (+4.7 KB)
Source maps 2.6 MB (+59 KB) 876 KB (+20 KB)
Other 11 KB 3.7 KB
Total 4.3 MB (+95 KB) 1.5 MB (+33 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 111 KB (+4.6 KB) 2.5 KB (+1 B) 113 KB (+4.6 KB) external 357 KB (+13 KB)
./beta 102 KB (-567 B) 485 B 102 KB (-567 B) external 306 KB (-2.4 KB)
./testing 45 KB (+271 B) 37 KB (+4.3 KB) 82 KB (+4.6 KB) external 237 KB (+13 KB)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 30 KB (+4.2 KB) 0 B 30 KB (+4.2 KB) external 87 KB (+12 KB)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 106 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 82 KB
./beta modular.js initial 6.0 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta client.js initial 647 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 178 B
./beta index.js lazy 115 B
./testing manifest.js initial 32 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 32 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 30 KB

@databricks/appkit-ui

npm tarball (packed): 395 KB (-2.3 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 427 KB (-1.9 KB) 146 KB (-761 B)
Type declarations 266 KB (-900 B) 98 KB (-210 B)
Source maps 849 KB (-4.4 KB) 284 KB (-1.4 KB)
CSS 16 KB 3.2 KB
Total 1.5 MB (-7.2 KB) 531 KB (-2.4 KB)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 2.1 KB (+15 B) 0 B 2.1 KB (+15 B) 0 B 5.0 KB (+22 B)
./react 433 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 4.7 KB (-184 B) 0 B 4.7 KB (-184 B) 0 B 12 KB (-539 B)

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 2.1 KB
./react index.js initial 431 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 4.7 KB

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 39974601194 -R databricks/appkit -n appkit-template-0.89.0-pr.734b3de-stack-database-stable-04-operation-generation-690 -D appkit-pr-690 \
  && unzip -o "appkit-pr-690/appkit-template-0.89.0-pr.734b3de-stack-database-stable-04-operation-generation-690.zip" -d "appkit-pr-690" \
  && databricks apps init --template "appkit-pr-690"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant