Skip to content
3 changes: 2 additions & 1 deletion experimental/aitools/lib/prompts/target_apps.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* L2: Target-specific guidance for Databricks Apps.
*
* Injected when: target type "apps" is detected or after init-template.
* Contains: app naming constraints, validation, deployment consent.
* Contains: validation, deployment consent, operational commands.
* AppKit-specific guidance is in skills/apps/appkit/ - load via read_skill_file.
*/ -}}

## Databricks Apps Development
Expand Down
Empty file.
139 changes: 139 additions & 0 deletions experimental/aitools/lib/skills/apps/appkit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
name: appkit
description: Build full-stack TypeScript Databricks Apps using @databricks/appkit (backend) and @databricks/appkit-ui (frontend). Use for analytics dashboards, data apps, SQL query visualization, SSE streaming, and Arrow data processing.
---

# AppKit - Full-Stack TypeScript Databricks Apps

Build **full-stack TypeScript apps** on Databricks using:

- **Backend**: `@databricks/appkit`
- **Frontend**: `@databricks/appkit-ui`
- **Analytics**: SQL files in `config/queries/*.sql` executed via the AppKit analytics plugin

## Hard Rules (LLM Guardrails)

- **Do not invent APIs**. Stick to patterns shown here and documented exports only.
- **`createApp()` is async**. Use **top-level `await createApp(...)`**.
- **Always memoize query parameters** passed to `useAnalyticsQuery` / charts to avoid refetch loops.
- **Always handle loading/error/empty states** in UI (use `Skeleton`, error text, empty state).
- **Always use `sql.*` helpers** for query parameters (do not pass raw strings/numbers).
- **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`.
- **Never use `require()`**. Use ESM `import/export`.
- **Charts do NOT accept children**. Use props (`xKey`, `yKey`, `colors`) NOT Recharts children.

## TypeScript Import Rules

With `verbatimModuleSyntax: true`, **always use `import type` for type-only imports**:

```ts
import type { ReactNode } from "react";
import { useMemo } from "react";
```

## Detailed Documentation

- **Backend**: [appkit-backend.md](appkit-backend.md)
- **Frontend**: [appkit-frontend.md](appkit-frontend.md)
- **Project Setup**: [appkit-scaffolding.md](appkit-scaffolding.md)

## Quick Start

### Minimal Server

```ts
// server/index.ts
import { createApp, server, analytics } from "@databricks/appkit";

await createApp({
plugins: [server(), analytics({})],
});
```

### Minimal Frontend

```tsx
// client/src/App.tsx
import { BarChart, Card, CardHeader, CardTitle, CardContent } from "@databricks/appkit-ui/react";
import { useMemo } from "react";

export default function App() {
const params = useMemo(() => ({}), []);

return (
<Card>
<CardHeader>
<CardTitle>Sales by Region</CardTitle>
</CardHeader>
<CardContent>
<BarChart queryKey="sales_by_region" parameters={params} />
</CardContent>
</Card>
);
}
```

### SQL Query

```sql
-- config/queries/sales_by_region.sql
SELECT region, SUM(revenue) as revenue
FROM sales
GROUP BY region
```

## Common Patterns

### useAnalyticsQuery (Custom UI Only)

Use only when you need custom UI (cards/KPIs/conditional rendering):

```tsx
import { useMemo } from "react";
import { useAnalyticsQuery, Skeleton } from "@databricks/appkit-ui/react";
import { sql } from "@databricks/appkit-ui/js";

export function Users() {
const params = useMemo(
() => ({
status: sql.string("active"),
limit: sql.number(50),
}),
[],
);

const { data, loading, error } = useAnalyticsQuery("users_list", params);

if (loading) return <Skeleton className="h-24 w-full" />;
if (error) return <div className="text-destructive">Error: {error}</div>;
if (!data || data.length === 0) return <div>No results</div>;

return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```

**Limitations:**
- No `enabled` option. Use conditional rendering to mount/unmount.
- No `refetch()`. Change `parameters` (memoized) or re-mount.

### Avoid Double-Fetching

```tsx
// ❌ Wrong: fetches the same query twice
const { data } = useAnalyticsQuery("spend_data", params);
return <LineChart queryKey="spend_data" parameters={params} />;

// ✅ Correct: let the chart fetch
return <LineChart queryKey="spend_data" parameters={params} />;
```

## LLM Checklist

Before finalizing code:

- [ ] `package.json` has `"type": "module"`
- [ ] `dev` script uses `NODE_ENV=development tsx watch server/index.ts`
- [ ] `await createApp({ plugins: [...] })` is used
- [ ] Charts use props NOT children
- [ ] Query parameters are memoized with `useMemo`
- [ ] Loading/error/empty states are explicit
213 changes: 213 additions & 0 deletions experimental/aitools/lib/skills/apps/appkit/appkit-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# AppKit Backend (@databricks/appkit)

## Server Plugin (`server()`)

What it does:
- Starts an Express server (default `host=0.0.0.0`, `port=8000`)
- Mounts plugin routes under `/api/<pluginName>/...`
- Adds `/health` (returns `{ status: "ok" }`)
- Serves frontend:
- **Development** (`NODE_ENV=development`): runs a Vite dev server in middleware mode
- **Production**: auto-detects static frontend directory

```ts
import { createApp, server } from "@databricks/appkit";

await createApp({
plugins: [
server({
port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000
host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0"
autoStart: true, // default: true
staticPath: "dist", // optional: force a specific static directory
}),
],
});
```

### Manual Server Start

When you need to extend Express:

```ts
import { createApp, server } from "@databricks/appkit";

const appkit = await createApp({
plugins: [server({ autoStart: false })],
});

appkit.server.extend((app) => {
app.get("/custom", (_req, res) => res.json({ ok: true }));
});

await appkit.server.start();
```

## Analytics Plugin (`analytics()`)

Add SQL query execution backed by Databricks SQL Warehouses:

```ts
import { analytics, createApp, server } from "@databricks/appkit";

await createApp({
plugins: [server(), analytics({})],
});
```

### SQL Queries

Put `.sql` files in `config/queries/`. Query key is filename without `.sql`:

```sql
-- config/queries/spend_summary.sql
-- @param startDate DATE
-- @param endDate DATE
-- @param limit NUMERIC
SELECT *
FROM usage
WHERE usage_date BETWEEN :startDate AND :endDate
LIMIT :limit
```

**Supported `-- @param` types:** STRING, NUMERIC, BOOLEAN, DATE, TIMESTAMP, BINARY

**Server-injected params:**
- `:workspaceId` is injected automatically and must NOT be annotated

### HTTP Endpoints

Mounted under `/api/analytics`:

- `POST /api/analytics/query/:query_key`
- `POST /api/analytics/users/me/query/:query_key`
- `GET /api/analytics/arrow-result/:jobId`

**Formats:**
- `format: "JSON"` (default) returns JSON rows
- `format: "ARROW"` returns Arrow statement_id over SSE

## Execution Context

### `asUser(req)` for User-Scoped Operations

```ts
// Execute as the user (uses their Databricks permissions)
router.post("/users/me/data", async (req, res) => {
const result = await this.asUser(req).query("SELECT ...");
res.json(result);
});

// Service principal execution (default)
router.post("/system/data", async (req, res) => {
const result = await this.query("SELECT ...");
res.json(result);
});
```

### Context Helper Functions

- `getExecutionContext()`: Returns current context (user or service)
- `getCurrentUserId()`: Returns user ID in user context
- `getWorkspaceClient()`: Returns appropriate WorkspaceClient
- `getWarehouseId()`: `Promise<string>`
- `getWorkspaceId()`: `Promise<string>`
- `isInUserContext()`: Returns `true` if in user context

## Custom Plugins

```ts
import { Plugin, toPlugin } from "@databricks/appkit";
import type express from "express";

class MyPlugin extends Plugin {
name = "my-plugin";
envVars = [];

injectRoutes(router: express.Router) {
this.route(router, {
name: "hello",
method: "get",
path: "/hello",
handler: async (_req, res) => {
res.json({ ok: true });
},
});
}
}

export const myPlugin = toPlugin<typeof MyPlugin, Record<string, never>, "my-plugin">(
MyPlugin,
"my-plugin",
);
```

## Caching

### Global Cache

```ts
await createApp({
plugins: [server(), analytics({})],
cache: {
enabled: true,
ttl: 3600, // seconds
strictPersistence: false,
},
});
```

### Plugin-Level Cache

```ts
// inside a Plugin subclass:
const value = await this.cache.getOrExecute(
["my-plugin", "data", userId],
async () => expensiveWork(),
userKey,
{ ttl: 300 },
);
```

## Environment Variables

### Required for Databricks Apps

| Variable | Description |
|----------|-------------|
| `DATABRICKS_HOST` | Workspace URL |
| `DATABRICKS_APP_PORT` | Port to bind (default: 8000) |
| `DATABRICKS_APP_NAME` | App name in Databricks |

### Required for SQL Queries

| Variable | How to Set |
|----------|------------|
| `DATABRICKS_WAREHOUSE_ID` | In `app.yaml`: `valueFrom: sql-warehouse` |

### Local Development Auth

**Option 1: Databricks CLI Auth (recommended)**

```bash
databricks auth login --host [host] --profile [profile-name]
DATABRICKS_CONFIG_PROFILE=my-profile npm run dev
```

**Option 2: Environment variables**

```bash
export DATABRICKS_HOST="https://xxx.cloud.databricks.com"
export DATABRICKS_TOKEN="dapi..."
export DATABRICKS_WAREHOUSE_ID="abc123..."
npm run dev
```

**Option 3: `.env` file**

```bash
# .env (add to .gitignore!)
DATABRICKS_HOST=https://xxx.cloud.databricks.com
DATABRICKS_TOKEN=dapi...
DATABRICKS_WAREHOUSE_ID=abc123...
```
Loading
Loading