Apps docs

Examples

Complete apps live in the Olan repository under examples/ (each passes validate --strict and test, with fixtures and tests to copy):

Example Shows
examples/project-pulse app surface, three screens on the navigation stack, status Badges, Menu → olan.projects.updateTask with revision, OlanError handling, toasts (projects:read, projects:write)
examples/overdue-widget project.widget surface with params.projectId, manifest settings + useSettings, date logic tested with a frozen clock (projects:read)
examples/exchange-rates olan.http.fetch with http:<host>, Table, Chart, a settings surface with a Select saving to olan.storage, HTTP fixtures, retry (http:rates.example.com, storage)
examples/decision-log olan.db with an organization and a user collection, useCollection with a status filter and loadMore, useRecord, "Göreve dönüştür" → olan.projects.createTask, deleteTask, deleteList with forbidden_record → toast, fixtures/db.json (projects:read, db, tasks:write, tasks:delete, projects:delete)
examples/ops-inbox Backend code (server, backend.md): olan.backend.call('summary') from the UI, a 5-minute schedules digest into olan.db, a project.task.created event handler, a GitHub hook verified with hmac-sha256, a secret: true setting read only by the backend (olan.secrets.get) and used for olan.http.fetch; createBackendHarness tests (db, projects:read, http:ops.example.com)
examples/media-desk SDK 1.5 "Medya Masası": olan.remote.request to its own Node server (handleRequest routes, RemoteError → Banner, 422 issues on fields), olan.files.pick + upload with ProgressBar and cancel, Image/Avatar from the server, openExternal sign-in that returns (appReturnUrl, state, useSurfaceParams<'app'>().return), an /apps/a/…/item?id= address, Tabs, Switch, MultiSelect, Stat, stacked area Chart, sortable Table, TextField maxLength, refetchIntervalMs; server tests for signature, stale timestamp, unknown issuer and replay (profile:read, files:pick)
examples/field-kit Every component, Form/TextField/Select/DatePicker, every olan.ui.* call, app + project.widget, settings, olan.db, task create/delete (the mobile acceptance app)

These directories are in the Olan source repository, not in node_modules, so an app project cannot open them; the table only says what each one shows. Everything you need is inline in these docs: the snippets below are complete surfaces, and the database pattern (collection, useCollection, insert/update/delete with revision, conflict/forbidden_record handling, fixtures/db.json) is in database.md ("API", "Errors", "Testing").

The olan-app init templates (blank, list-detail, data-list, project-widget, project-tab, project-records, external-api) are smaller starting points that land in your project: npx olan-app init "Notlar" --template data-list writes a complete olan.db app (list, add with a Form, detail, update, delete) with tests.

To install an example in a local Olan, pack it with your development key (npx olan-app pack --dev-sign, inside the example directory) after adding the key to the local Platform API (npx olan-app keygen --dev prints the OLAN_APPS_TRUSTED_KEYS line). A plain pack is unsigned: the catalog lists it, but it does not install (cli.md#signing-and-publishing).

A widget on the project page #

import { Card, EmptyState, List, defineApp, olan, useQuery, type SurfaceProps } from '@olan-app/sdk';

function OverdueCard({ params }: SurfaceProps) {
  const projectId = (params as { projectId?: string } | null)?.projectId ?? '';
  const tasks = useQuery(() => olan.projects.tasks(projectId), [projectId]);
  // A sketch: "today" here is the UTC date. A real app should compare in the project's
  // `timeZone` (olan.projects.get) and treat a due time (`2026-10-17T15:00`) as overdue once it passes.
  const today = new Date().toISOString().slice(0, 10);
  const overdue = (tasks.data ?? []).filter(
    (t) => (t.status === 'needs-action' || t.status === 'in-process') && t.due !== null && t.due.slice(0, 10) < today,
  );
  if (tasks.error) return <EmptyState title="Görevler yüklenemedi" description={tasks.error.message} />;
  if (!tasks.loading && overdue.length === 0) return <EmptyState title="Geciken görev yok" icon="check-circle" />;
  return (
    <Card title="Geciken görevler" subtitle={`${overdue.length} görev`}>
      <List loading={tasks.loading} items={overdue.map((t) => ({ key: t.id, title: t.title, meta: t.due ?? undefined, badge: { text: 'Gecikti', tone: 'danger' } }))} />
    </Card>
  );
}

export default defineApp({ 'project.widget': OverdueCard });

Manifest: "surfaces": { "project.widget": { "title": "Gecikenler" } }, "permissions": ["projects:read"].

Changing a task from a menu #

const STATUS_LABEL: Record<ProjectTaskStatus, string> = { 'needs-action': 'Yapılacak', 'in-process': 'Sürüyor', completed: 'Tamamlandı', cancelled: 'İptal' };

function TaskScreen({ projectId, taskId }: { projectId: string; taskId: string }) {
  const detail = useQuery(() => olan.projects.get(projectId), [projectId]);
  const task = detail.data?.tasks.find((t) => t.id === taskId);
  if (!task) return <Screen title="Görev" loading={detail.loading} />;
  return (
    <Screen title={task.title}>
      <Stack gap="md" padding="md">
        <Badge text={STATUS_LABEL[task.status]} />
        <Menu
          label="Durumu değiştir"
          items={(Object.keys(STATUS_LABEL) as ProjectTaskStatus[]).map((s) => ({ key: s, label: STATUS_LABEL[s], disabled: s === task.status }))}
          onSelect={async ({ key }) => {
            await olan.projects.updateTask(projectId, task.id, { status: key as ProjectTaskStatus, revision: task.revision });
            await olan.ui.toast({ text: 'Güncellendi', tone: 'success' });
            await detail.refetch();
          }}
        />
      </Stack>
    </Screen>
  );
}

Permissions: projects:read, tasks:write (apps written before SDK 1.1 use projects:write, which still allows updateTask).

A form that saves to storage #

function Notes() {
  const notes = useQuery(async () => (await olan.storage.get<string[]>('notes')) ?? []);
  const add = async (text: string) => {
    if (!text.trim()) return;
    await olan.storage.set('notes', [...(notes.data ?? []), text.trim()]);
    await notes.refetch();
  };
  return (
    <Screen title="Notlar" loading={notes.loading}>
      <Stack gap="md" padding="md">
        <Form submitLabel="Ekle" onSubmit={({ values }) => add(values.text ?? '')}>
          <TextField name="text" label="Not" placeholder="Bir not yazın" />
        </Form>
        <List items={(notes.data ?? []).map((text, index) => ({ key: index, title: text }))} emptyText="Not yok" />
      </Stack>
    </Screen>
  );
}

export default defineApp({ app: Notes });

Permission: storage. Test: await app.fill('Not', 'Süt al'); await app.press('Ekle'); app.expectText('Süt al');.

An external API #

const URL = 'https://status.example.com/api/services.json';   // permission "http:status.example.com"

function Services() {
  const services = useQuery(async () => {
    const response = await olan.http.fetch(URL, { headers: { accept: 'application/json' } });
    if (response.status !== 200) throw new Error(`API ${response.status}`);
    return response.json<{ services: { id: string; name: string; up: boolean }[] }>().services;
  });
  return (
    <Screen title="Servisler" loading={services.loading} actions={[{ key: 'refresh', label: 'Yenile', icon: 'refresh', onPress: () => services.refetch() }]}>
      {services.error ? (
        <EmptyState title="Durum alınamadı" description={services.error.message} action={{ label: 'Tekrar dene', onPress: () => services.refetch() }} />
      ) : (
        <List items={(services.data ?? []).map((s) => ({ key: s.id, title: s.name, badge: s.up ? { text: 'Çalışıyor', tone: 'success' } : { text: 'Kapalı', tone: 'danger' } }))} />
      )}
    </Screen>
  );
}

fixtures/http.json: { "https://status.example.com/api/services.json": { "status": 200, "json": { "services": [ … ] } } }.

Confirm before a destructive action #

<Button
  label="Hepsini temizle"
  variant="danger"
  onPress={async () => {
    if (!(await olan.ui.confirm({ title: 'Tüm notlar silinsin mi?', confirmLabel: 'Sil', destructive: true }))) return;
    await olan.storage.remove('notes');
    await notes.refetch();
  }}
/>

Test the "cancel" path with renderApp({ ui: { confirm: false } }). Review guidelines 2.1 (nothing is deleted without asking) and 4.1 (labelled controls) in review-guidelines.md.