> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an inventory tracker

> A complete walk-through: a stock-management app with a database, backend workflows, a product list with an adjust dialog, camera scan-to-find, low-stock alerts, team-wide access, and publishing.

You build it by describing what you want to an agent through the [Zite MCP](/mcp/overview); the code below is what it generates.

## Describe the app

```text theme={null}
Create a workspace called "Inventory", then build a stock-management app.
Products have a name, SKU, price, quantity on hand, and a category. Every
stock change is logged as a movement (which product, a +/- delta, a reason).
I want a searchable product list, a dialog to adjust stock, the ability to
scan a barcode to find a product, and a daily alert when items run low.
```

## Data model

Two tables: `Products`, and `StockMovements` linked to products — an append-only log so every quantity change is auditable. Field/table names become **SDK names** in code (`StockMovements` → `zite.StockMovements`, "SKU" → `sku`); every table also has a system `createdAt` you can sort on. See [Databases](/concepts/databases).

| Table              | Field (SDK name) | Type                                                     |
| ------------------ | ---------------- | -------------------------------------------------------- |
| **Products**       | `name`           | single\_line\_text (primary)                             |
|                    | `sku`            | single\_line\_text                                       |
|                    | `price`          | currency                                                 |
|                    | `quantity`       | number                                                   |
|                    | `category`       | single\_select — Electronics / Apparel / Grocery / Other |
| **StockMovements** | `reason`         | single\_line\_text (primary)                             |
|                    | `product`        | linked\_record → Products                                |
|                    | `delta`          | number                                                   |
|                    | `createdAt`      | *system* — every record has one                          |

## Workflows

One file per workflow in `src/api/`. Listing supports a category filter, a name search, and an exact-SKU lookup (the last powers scan-to-find):

```typescript theme={null}
// apps/inventory/src/api/listProducts.ts
import { z } from 'zod';
import { createEndpoint } from 'zitejs/backend';
import { zite } from 'zitejs/db';

export default createEndpoint({
  description: 'List products, optionally filtered by category, name search, or exact SKU',
  authenticated: true,
  inputSchema: z.object({
    category: z.string().optional(),
    search: z.string().optional(),
    sku: z.string().optional(),
  }),
  outputSchema: z.object({
    products: z.array(z.object({
      id: z.string(),
      name: z.string(),
      sku: z.string(),
      price: z.number().nullable(),
      quantity: z.number().nullable(),
      category: z.string().nullable(),
    })),
  }),
  execute: async ({ input }) => {
    const filter: Record<string, unknown> = {};
    if (input.category) filter.category = input.category;
    if (input.sku) filter.sku = input.sku;
    if (input.search) filter.name = { contains: input.search };

    const { records } = await zite.Products.findAll({
      filter: Object.keys(filter).length ? filter : undefined,
      sort: [{ field: 'name', direction: 'asc' }],
      limit: 500,
    });
    return {
      products: records.map(p => ({
        id: p.id, name: p.name, sku: p.sku,
        price: p.price, quantity: p.quantity, category: p.category,
      })),
    };
  },
});
```

Adjusting stock does two writes — it appends a `StockMovements` record (the audit trail) and updates the product's `quantity`. Linked-record values are an **array of ids**:

```typescript theme={null}
// apps/inventory/src/api/adjustStock.ts
import { z } from 'zod';
import { createEndpoint, ZiteError } from 'zitejs/backend';
import { zite } from 'zitejs/db';

export default createEndpoint({
  description: 'Record a stock movement and update the product quantity',
  authenticated: true,
  inputSchema: z.object({
    productId: z.string(),
    delta: z.number().int(),
    reason: z.string().min(1),
  }),
  outputSchema: z.object({ quantity: z.number() }),
  execute: async ({ input }) => {
    const product = await zite.Products.findOne({ id: input.productId });
    if (!product) throw new ZiteError('Product not found', { statusCode: 404 });

    const quantity = (product.quantity ?? 0) + input.delta;
    if (quantity < 0) throw new ZiteError('Not enough stock on hand', { statusCode: 400 });

    await zite.StockMovements.create({
      record: { product: [input.productId], delta: input.delta, reason: input.reason },
    });
    await zite.Products.update({ id: input.productId, record: { quantity } });
    return { quantity };
  },
});
```

Add workflows, then run `npx zitejs generate` so the typed frontend client picks them up. See [Workflows](/framework/workflows).

## Frontend

A searchable list with a low-stock badge and an inline **Adjust** button per product:

```tsx theme={null}
// apps/inventory/src/App.tsx
import { useEffect, useState } from 'react';
import { listProducts, ListProductsOutputType } from 'zitejs/api';
import { Input } from '@project/components/ui/input';
import { Button } from '@project/components/ui/button';
import { Badge } from '@project/components/ui/badge';
import { AdjustStockDialog } from './components/AdjustStockDialog';

type Product = ListProductsOutputType['products'][number];

export default function App() {
  const [search, setSearch] = useState('');
  const [products, setProducts] = useState<Product[]>([]);
  const [adjusting, setAdjusting] = useState<Product>();

  const load = () => listProducts({ search: search || undefined }).then(d => setProducts(d.products));
  useEffect(() => { load(); }, [search]);

  return (
    <div className="p-6 space-y-4">
      <Input placeholder="Search products…" value={search} onChange={e => setSearch(e.target.value)} />
      {products.map(p => (
        <div key={p.id} className="flex items-center justify-between border rounded-lg p-4">
          <div>
            <div className="font-medium">{p.name}</div>
            <div className="text-sm text-muted-foreground">{p.sku}</div>
          </div>
          <div className="flex items-center gap-3">
            <Badge variant={(p.quantity ?? 0) <= 10 ? 'destructive' : 'secondary'}>
              {p.quantity ?? 0} in stock
            </Badge>
            <Button variant="outline" onClick={() => setAdjusting(p)}>Adjust</Button>
          </div>
        </div>
      ))}
      {adjusting && (
        <AdjustStockDialog
          product={adjusting}
          onClose={() => setAdjusting(undefined)}
          onDone={() => { setAdjusting(undefined); load(); }}
        />
      )}
    </div>
  );
}
```

`AdjustStockDialog` collects a signed delta and a reason, then calls `adjustStock`.

## Permissions

Everyone on the team can read and write. Grant the built-in **All team members** role full access to both tables:

```json theme={null}
{
  "version": "1.0",
  "defaultPolicy": "deny",
  "roles": [
    { "id": "builtin:all-team-members", "name": "All team members" }
  ],
  "tables": {
    "Products": {
      "rules": [
        { "roles": ["All team members"], "operations": ["read", "create", "update", "delete"] }
      ]
    },
    "StockMovements": {
      "rules": [
        { "roles": ["All team members"], "operations": ["read", "create", "update", "delete"] }
      ]
    }
  }
}
```

`defaultPolicy: "deny"` locks everything down; these two rules open both tables to the whole team. See the [permissions model](/concepts/permissions) and [file schema](/framework/permissions-file).

## Scan-to-find & low-stock alerts

* **Scan a barcode to jump to a product** — drop in the camera scanner and look the scanned SKU up through `listProducts({ sku })`. See the [Barcode scanning recipe](/recipes/barcode-scanning).
* **Alert the team when stock runs low** — add a `schedule` to a workflow that counts low products and notifies the team daily. See [Scheduled jobs](/recipes/scheduled-jobs).

## Publish

Ask the agent to `Publish the app`. Internal apps go live to your team; external apps get a `zite.so` URL with sign-in. See [Publishing](/deploy/publishing).
