Features
Core features of the apx development toolkit
apx provides a comprehensive set of features designed to streamline Databricks App development for both humans and AI assistants.
Development Server
The apx dev server provides a seamless full-stack development experience with hot reloading, automatic API client generation, and detached mode operation.
Key Features
Hot Reload
Automatic refresh on code changes for both frontend and backend
Detached Mode
Run servers in background, continue working in terminal
Unified Logs
OpenAPI Sync
Auto-generate TypeScript client from Python models
Quick Start
# Start all servers in detached mode
apx dev start
# Check status
apx dev status
# View logs
apx dev logs -f
# Stop when done
apx dev stopArchitecture
When you run dev start, apx launches:
- Backend Server - FastAPI application with hot reload via uvicorn
- Frontend Server - Vite dev server with HMR (Hot Module Replacement)
- OpenAPI Watcher - Monitors Python models and regenerates TypeScript client
- Database Sidecar - PGlite in-memory PostgreSQL instance (for lakebase addon)
All services run in detached mode, allowing you to continue working in your terminal. Logs are collected and can be streamed with dev logs -f.
Using Generated API Client
The generated TypeScript client provides type-safe hooks for calling your API. Import from @/lib/api:
// Query hook for GET requests
import { useListItems, useListItemsSuspense } from "@/lib/api";
const Component = () => {
const { data, isLoading, error } = useListItems();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{/* render data */}</div>;
};
// Suspense variant (use with React Suspense boundary)
const SuspenseComponent = () => {
const { data } = useListItemsSuspense();
return <div>{/* render data */}</div>;
};
// Mutation hook for POST/PUT/DELETE requests
import { useCreateItem } from "@/lib/api";
const FormComponent = () => {
const { mutate, isPending } = useCreateItem();
const handleSubmit = () => {
mutate({ data: { name: "New Item" } });
};
return <button onClick={handleSubmit}>Create</button>;
};Types are also exported for use in your components:
import type { ListItemsQueryResult, CreateItemMutationBody } from "@/lib/api";OpenAPI Integration
apx automatically generates a TypeScript API client from your FastAPI backend:
# backend/models.py
from pydantic import BaseModel
class Order(BaseModel):
id: str
customer_name: str
total: floatThis automatically generates:
// Generated TypeScript client
interface Order {
id: string;
customer_name: string;
total: number;
}The watcher monitors your Python files and regenerates the client whenever you modify Pydantic models or FastAPI routes.
Automatic Tool Management
apx automatically manages its runtime dependencies — uv (Python package manager) and bun (JavaScript runtime) — so you don't need to install them manually.
Resolution Order
When apx needs uv or bun, it resolves the binary in this order:
Environment variable override APX_UV_PATH or APX_BUN_PATH — use a
custom binary path.
Download Details
- Binaries are downloaded from official GitHub releases
- SHA-256 checksums are verified after download
- Downloads are platform-aware (macOS x64/ARM, Linux x64/ARM, Windows x64)
- A version marker file tracks the installed version to avoid re-downloading
Environment Variable Overrides
| Variable | Description |
|---|---|
APX_UV_PATH | Path to a custom uv binary |
APX_BUN_PATH | Path to a custom bun binary |
Built-in Components CLI
apx provides a CLI for working with shadcn registries, allowing you to add components from any shadcn-compatible registry to your project. Additional curated registries for animations, AI components, and icons are preconfigured.
Note: apx does not use a components.json file. Component registries are
configured in pyproject.toml under the [tool.apx.ui.registries] section.
The shadcn/ui registry is used as the default, so
there's no need to add it manually.
Adding Components
apx components add button
apx components add card
apx components add dialog
apx components add @animate-ui/sidebar # Add a component from a specific registryAvailable Registries
apx comes preconfigured with high-quality component registries:
| Repository | Alias | Description | License |
|---|---|---|---|
| shadcn/ui | (default) | Core UI components | MIT |
| animate-ui | @animate-ui | Animation components | MIT |
| ai-sdk | @ai-elements | AI components (chat, prompts) | Apache-2.0 |
| svgl | @svgl | SVG icons collection | MIT |
Example: Building a Dashboard
# Add required components
apx components add card
apx components add table
apx components add chart
apx components add tabsThen use them in your React code:
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
export function Dashboard() {
return (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Orders</CardTitle>
</CardHeader>
<CardContent>
<Table>{/* ... */}</Table>
</CardContent>
</Card>
</div>
);
}Customization
Components are installed directly into your project at src/<app>/ui/components/, giving you full control to customize styles and behavior.
Registry configuration is stored in pyproject.toml:
[tool.apx.ui.registries]
"@animate-ui" = "https://animate-ui.com/r/{name}.json"
"@ai-elements" = "https://registry.ai-sdk.dev/{name}.json"
"@svgl" = "https://svgl.app/r/{name}.json"You can add any shadcn-compatible registry by adding its URL pattern to this section.
Local Development Database
The lakebase addon includes integrated database support via SQLModel, with PGlite running as an in-memory PostgreSQL sidecar during development that maps to Lakebase in production.
Initializing with Database Support
Select the lakebase addon during apx init, or apply to an existing project:
apx dev apply lakebaseHow It Works
When you run apx dev start with the lakebase addon, apx automatically spins up a PGlite instance as a sidecar process. PGlite provides a fully PostgreSQL-compatible database running in-memory, giving you the same SQL dialect and features you'll have in production with Lakebase.
Defining Models
SQLModel combines Pydantic and SQLAlchemy for type-safe database models:
from sqlmodel import SQLModel, Field
from typing import Optional
from datetime import datetime
class Order(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
customer_name: str
total: float
status: str = "pending"
created_at: datetime = Field(default_factory=datetime.utcnow)Database Operations
from sqlmodel import select
from .core import Dependencies, create_router
from .models import Order
router = create_router()
@router.get("/orders", response_model=list[Order], operation_id="getOrders")
def get_orders(session: Dependencies.Session):
return session.exec(select(Order)).all()
@router.post("/orders", response_model=Order, operation_id="createOrder")
def create_order(order: Order, session: Dependencies.Session):
session.add(order)
session.commit()
session.refresh(order)
return orderDevelopment vs Production
| Environment | Database | Configuration |
|---|---|---|
| Development | PGlite | In-memory PostgreSQL sidecar |
| Production | Lakebase | Configured via PGAPPNAME environment variable inside Apps runtime |
The same SQLModel code works in both environments - apx handles the connection configuration automatically. Since PGlite is PostgreSQL-compatible, you get the same SQL behavior in development as you will in production.
Dependency Injection Pattern
apx provides a clean, type-safe dependency injection pattern through the Dependency class in backend/core.py. This pattern simplifies access to common resources like Databricks clients, configuration, and database sessions.
Available Dependencies
All dependencies are accessed via the Dependencies class:
| Dependency | Type | Description | Recommended Usage |
|---|---|---|---|
Dependencies.Client | WorkspaceClient | App-level Databricks client (service principal) | ws: Dependencies.Client |
Dependencies.UserClient | WorkspaceClient | User-scoped Databricks client (OBO token) | user_ws: Dependencies.UserClient |
Dependencies.Config | AppConfig | Application configuration | config: Dependencies.Config |
Dependencies.Session | Session | Database session (lakebase addon only) | session: Dependencies.Session |
Basic Usage
Import Dependencies and create_router from .core and use them in your route handlers:
from .core import Dependencies, create_router
router = create_router()
@router.get("/clusters", response_model=list[ClusterOut], operation_id="listClusters")
def list_clusters(ws: Dependencies.Client):
"""List clusters using app-level credentials."""
return ws.clusters.list()
@router.get("/current-user", response_model=UserOut, operation_id="currentUser")
def get_current_user(user_ws: Dependencies.UserClient):
"""Get current user info using their token."""
return user_ws.current_user.me()
@router.get("/config", response_model=AppSettingsOut, operation_id="getAppConfig")
def get_app_config(config: Dependencies.Config):
"""Access application configuration."""
return AppSettingsOut(app_name=config.app_name)Always use create_router() instead of APIRouter() directly — it
automatically sets the correct API prefix for your app.
User-Scoped Operations (OBO)
The Dependencies.UserClient provides on-behalf-of (OBO) authentication, allowing you to perform operations as the authenticated user. This requires the X-Forwarded-Access-Token header (automatically provided by Databricks Apps):
@router.get("/my-jobs", response_model=list[JobOut], operation_id="getMyJobs")
def get_my_jobs(user_ws: Dependencies.UserClient):
"""List jobs visible to the current user."""
jobs = user_ws.jobs.list(name="my-job-prefix")
return [{"id": j.job_id, "name": j.settings.name} for j in jobs]
@router.post("/create-notebook", response_model=NotebookOut, operation_id="createNotebook")
def create_notebook(user_ws: Dependencies.UserClient):
"""Create a notebook in the user's workspace."""
result = user_ws.workspace.import_(
path="/Users/me/my-notebook",
content="# My Notebook",
format="SOURCE",
language="PYTHON"
)
return {"path": result.path}Multiple Dependencies
You can combine multiple dependencies in a single route handler:
from sqlmodel import select
from .models import Order
from .core import Dependencies
@router.get("/orders/stats", response_model=OrderStatsOut, operation_id="getOrderStats")
def get_order_stats(
session: Dependencies.Session,
user_ws: Dependencies.UserClient,
config: Dependencies.Config
):
"""Get order statistics for the current user."""
orders = session.exec(select(Order)).all()
user = user_ws.current_user.me()
return OrderStatsOut(
app=config.app_name,
user=user.user_name,
total_orders=len(orders),
total_revenue=sum(o.total for o in orders)
)Extending AppConfig
Add custom configuration fields to AppConfig in core.py. Fields are populated from environment variables with the {APP_SLUG}_ prefix:
class AppConfig(BaseSettings):
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
env_file=env_file,
env_prefix=f"{app_slug.upper()}_",
extra="ignore",
env_nested_delimiter="__",
)
app_name: str = Field(default=app_name)
# Custom fields - set via env vars like MYAPP_MY_API_KEY
my_api_key: str = Field(default="")
max_results: int = Field(default=100)Then access via Dependencies.Config:
@router.get("/search", response_model=list[SearchResult], operation_id="search")
def search(query: str, config: Dependencies.Config):
return external_api.search(query, api_key=config.my_api_key, limit=config.max_results)Custom Lifespan
Use the lifespan parameter in create_app for custom startup/shutdown logic. The default lifespan (which initializes app.state.config and app.state.workspace_client) runs first, so your custom lifespan can use them:
from contextlib import asynccontextmanager
from .core import create_app, create_router, Dependencies
router = create_router()
@asynccontextmanager
async def custom_lifespan(app):
# app.state.config and app.state.workspace_client are already available
app.state.my_resource = await init_something(app.state.config)
yield
await app.state.my_resource.cleanup()
app = create_app(routers=[router], lifespan=custom_lifespan)For apps using the lakebase addon, app.state.engine (SQLAlchemy engine) is
also available inside the custom lifespan — the database is initialized before
your lifespan runs.
Type Safety
The Dependencies class provides full type safety and IDE autocomplete. When you hover over a dependency in your IDE, you'll see:
- The actual type (e.g.,
WorkspaceClient,AppConfig,Session) - Documentation about what the dependency provides
- Recommended parameter naming convention
This makes it easy to discover available methods and catch type errors early.
Addons
Addons are composable building blocks that you can select during apx init or apply later with apx dev apply. They allow you to customize your project with only the features you need.
Addon Groups
| Group | Addon | Description | Default |
|---|---|---|---|
| UI | ui | Frontend with React, Vite, and TanStack Router | Yes |
sidebar | Sidebar navigation layout (depends on ui) | No | |
| Backend | sql | SQL Warehouse connection and query API | No |
lakebase | Lakebase (Postgres) integration with PGlite dev sidecar | No | |
| Assistants | cursor | Cursor IDE rules and MCP config | No |
vscode | VS Code instructions and MCP config | No | |
claude | Claude Code project rules and MCP config | No | |
codex | OpenAI Codex AGENTS.md file | No |
Addon Dependencies
Some addons depend on others. When an addon has a dependency, the dependent addon must also be enabled:
sidebardepends onui— it adds a sidebar navigation layout to the UI frontendlakebaseandsqlare standalone backend addons with no cross-dependencies
How Addons Work
Addons are applied using AST-based code generation. When an addon is selected during apx init or applied via apx dev apply:
- Template files are rendered and copied into your project
- Python AST edits are applied — imports and
TypeAliasmembers (used for theDependencyclass) are added automatically to existing source files - Component installation runs for any UI components declared in the addon manifest
This approach ensures that addons integrate cleanly with your existing code without manual copy-paste or merge conflicts.
Usage
Select addons during initialization:
apx init --addons=ui,sidebar,cursorCreate a backend-only project with no addons:
apx init --no-addonsApply an addon to an existing project:
apx dev apply lakebase
apx dev apply cursorWhen no --addons flag is given, an interactive multi-select prompt with group headers is shown.