apx

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

Stream logs from all services in one place

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 stop

Architecture

When you run dev start, apx launches:

  1. Backend Server - FastAPI application with hot reload via uvicorn
  2. Frontend Server - Vite dev server with HMR (Hot Module Replacement)
  3. OpenAPI Watcher - Monitors Python models and regenerates TypeScript client
  4. 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: float

This 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:

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

VariableDescription
APX_UV_PATHPath to a custom uv binary
APX_BUN_PATHPath 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 registry

Available Registries

apx comes preconfigured with high-quality component registries:

RepositoryAliasDescriptionLicense
shadcn/ui(default)Core UI componentsMIT
animate-ui@animate-uiAnimation componentsMIT
ai-sdk@ai-elementsAI components (chat, prompts)Apache-2.0
svgl@svglSVG icons collectionMIT

Example: Building a Dashboard

# Add required components
apx components add card
apx components add table
apx components add chart
apx components add tabs

Then 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 lakebase

How 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 order

Development vs Production

EnvironmentDatabaseConfiguration
DevelopmentPGliteIn-memory PostgreSQL sidecar
ProductionLakebaseConfigured 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:

DependencyTypeDescriptionRecommended Usage
Dependencies.ClientWorkspaceClientApp-level Databricks client (service principal)ws: Dependencies.Client
Dependencies.UserClientWorkspaceClientUser-scoped Databricks client (OBO token)user_ws: Dependencies.UserClient
Dependencies.ConfigAppConfigApplication configurationconfig: Dependencies.Config
Dependencies.SessionSessionDatabase 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

GroupAddonDescriptionDefault
UIuiFrontend with React, Vite, and TanStack RouterYes
sidebarSidebar navigation layout (depends on ui)No
BackendsqlSQL Warehouse connection and query APINo
lakebaseLakebase (Postgres) integration with PGlite dev sidecarNo
AssistantscursorCursor IDE rules and MCP configNo
vscodeVS Code instructions and MCP configNo
claudeClaude Code project rules and MCP configNo
codexOpenAI Codex AGENTS.md fileNo

Addon Dependencies

Some addons depend on others. When an addon has a dependency, the dependent addon must also be enabled:

  • sidebar depends on ui — it adds a sidebar navigation layout to the UI frontend
  • lakebase and sql are 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:

  1. Template files are rendered and copied into your project
  2. Python AST edits are applied — imports and TypeAlias members (used for the Dependency class) are added automatically to existing source files
  3. 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,cursor

Create a backend-only project with no addons:

apx init --no-addons

Apply an addon to an existing project:

apx dev apply lakebase
apx dev apply cursor

When no --addons flag is given, an interactive multi-select prompt with group headers is shown.

On this page