Docs

Schema Studio - Technical Design Document

A fast, local-first, in-browser workbench for Markdown, Avro binary data (.avro), and Avro schemas (.avsc), with optional cloud sharing for Markdown documents.


Table of Contents

  1. Overview
  2. Goals & Non-Goals
  3. High-Level Architecture
  4. Tech Stack
  5. Project Structure
  6. Routing Model
  7. Application Shell & UX
  8. Feature Module: Markdown Studio
  9. Feature Module: AVSC Viewer (Schema Studio)
  10. Feature Module: Avro Viewer (Data Studio)
  11. Avro Decoder Internals
  12. Large-Dataset Pipeline (Worker + IndexedDB)
  13. Sharing Subsystem
  14. Authentication
  15. Persistence & Storage
  16. Theming
  17. Server & SSR
  18. Security Considerations
  19. Performance Characteristics
  20. Error Handling & Observability
  21. Configuration & Environment
  22. Build, Dev, and Deployment
  23. Database Schema (Supabase)
  24. Extensibility Notes

1. Overview

Schema Studio is a single-page, SSR-capable React application that bundles three utilities into one workbench:

The app is local-first: uploaded files are decoded and rendered client-side and never sent to a server. An optional Markdown sharing feature uses Supabase to persist shareable, password-protectable links.


2. Goals & Non-Goals

Goals

Non-Goals


3. High-Level Architecture

flowchart LR
  subgraph Browser
    UI[React UI · TanStack Router]
    MD[Markdown Studio]
    AVSC[AVSC Viewer]
    AVRO[Avro Viewer]
    W[Web Worker · avro-worker.ts]
    IDB[(IndexedDB · chunks)]
    LS[(localStorage · prefs & drafts)]
  end

  subgraph Server[SSR / Serverless]
    SSR[TanStack Start entry · src/server.ts]
    FN[Server Functions · createServerFn]
  end

  subgraph Supabase
    AUTH[Auth]
    DB[(Postgres · public.shares)]
    RT[Realtime]
  end

  UI --> MD
  UI --> AVSC
  UI --> AVRO
  AVRO --> W --> IDB
  UI <-.persist prefs.-> LS
  UI -->|share / list / update| FN
  FN --> DB
  FN --> AUTH
  UI --> SSR
  UI <-.subscribe.-> RT

4. Tech Stack

Layer Technology
Framework TanStack Start (React + SSR + server fns)
Router TanStack Router (file-based routes)
Data TanStack Query
UI kit shadcn/ui (Radix Primitives) + Tailwind CSS v4 + tw-animate-css
Icons lucide-react
Markdown marked, dompurify
Diagrams mermaid
PDF jspdf + html2canvas
Forms react-hook-form + zod + @hookform/resolvers
Toasts sonner
Auth / DB @supabase/supabase-js, @lovable.dev/cloud-auth-js
Build Vite 8, nitro (Cloudflare Worker target), TypeScript 5
Lint / format ESLint 9 + typescript-eslint, Prettier

5. Project Structure

schema-studio-main/
├── src/
│   ├── router.tsx              # Router bootstrap (QueryClient injection)
│   ├── routeTree.gen.ts        # Auto-generated route tree (do not edit)
│   ├── server.ts               # Nitro/SSR entry with h3 error normalization
│   ├── start.ts                # TanStack Start bootstrap (global fn middleware)
│   ├── styles.css              # Tailwind entry
│   ├── components/
│   │   ├── app-sidebar.tsx     # Left navigation
│   │   ├── studios.tsx         # Markdown + AVSC + Avro data studios
│   │   ├── JsonTree.tsx        # Recursive JSON tree + record-array table view
│   │   ├── share-dialog.tsx    # Share creation UI
│   │   └── ui/*                # shadcn/ui primitives
│   ├── hooks/
│   │   ├── use-auth.tsx        # Supabase auth context
│   │   ├── use-mobile.tsx
│   │   └── use-theme.tsx       # light / dark / system theme
│   ├── integrations/
│   │   ├── lovable/index.ts    # OAuth (Google) via Lovable Cloud
│   │   └── supabase/
│   │       ├── client.ts             # Browser client (proxy-lazy)
│   │       ├── client.server.ts
│   │       ├── auth-middleware.ts    # `requireSupabaseAuth` server middleware
│   │       ├── auth-attacher.ts      # Client fn middleware attaching bearer
│   │       └── types.ts              # Generated DB types
│   ├── lib/
│   │   ├── avro.ts             # Pure Avro OCF decoder
│   │   ├── avro-worker.ts      # Web Worker wrapping the streaming decoder
│   │   ├── avro-store.ts       # IndexedDB chunk store (paging)
│   │   ├── share.functions.ts  # Server fns for share CRUD
│   │   ├── error-capture.ts    # SSR error capture ring
│   │   ├── error-page.ts       # Fallback SSR error HTML
│   │   ├── lovable-error-reporting.ts
│   │   └── utils.ts            # cn() etc.
│   └── routes/
│       ├── __root.tsx          # App shell (sidebar, header, footer, providers)
│       ├── index.tsx           # `/` — Utilities hub
│       ├── markdown.tsx        # `/markdown`
│       ├── avsc-viewer.tsx     # `/avsc-viewer`
│       ├── avro-viewer.tsx     # `/avro-viewer`
│       ├── auth.tsx            # `/auth`
│       ├── s.$shareId.tsx      # `/s/:shareId` — public shared markdown
│       └── _authenticated/
│           ├── route.tsx       # Layout guard (redirects to /auth)
│           └── my-shares.tsx   # `/my-shares` — manage shares
├── supabase/
│   ├── config.toml
│   └── migrations/…            # `public.shares` schema + RLS
├── package.json
├── vite.config.ts
├── tsconfig.json
├── eslint.config.js
├── components.json             # shadcn/ui config
├── bunfig.toml
└── README.md

6. Routing Model

Routing is file-based. src/routeTree.gen.ts is generated by the TanStack Router plugin.

File URL Description
routes/__root.tsx App shell (sidebar, header, footer, providers, error/404)
routes/index.tsx / Utilities hub with 3 utility cards
routes/markdown.tsx /markdown Markdown editor
routes/avsc-viewer.tsx /avsc-viewer Avro schema inspector
routes/avro-viewer.tsx /avro-viewer Avro binary data reader
routes/auth.tsx /auth Sign in / sign up (email + Google OAuth)
routes/s.$shareId.tsx /s/:shareId Public shared markdown view/edit
routes/_authenticated/route.tsx /_authenticated Guard: beforeLoad redirects if not signed in
routes/_authenticated/my-shares.tsx /my-shares Owner-only share management

Each route sets its own SEO/OG <meta> tags via head(). The /s/:shareId route is marked noindex.


7. Application Shell & UX

src/routes/__root.tsx wraps every page in:

  1. Providers
    • QueryClientProvider (React Query)
    • ThemeProvider (light/dark/system)
    • AuthProvider (Supabase session)
    • <Toaster> (Sonner, top-right, richColors)
  2. Layout
    • SidebarProvider + AppSidebar (offcanvas collapsible)
    • Sticky header: SidebarTrigger, centered brand link, “Local · No upload to server” badge, upload menu (AddFileMenu), theme toggle
    • <Outlet /> inside a flex column <main>
    • Footer strip with subtle credits

Global upload event busAddFileMenu in the header dispatches a ss:trigger-upload CustomEvent after routing to the matching page. The target studio subscribes via useUploadListener(kind) and opens its hidden <input type="file">.

Pre-hydration theme script__root.tsx injects an inline <script> that reads localStorage["forge-theme"] and applies .light/.dark to <html> before React mounts to avoid FOUC.

Session reset on mountuseAvroSessionReset clears prior AVRO/AVSC localStorage keys and deletes the IndexedDB database on shell mount so datasets never leak across sessions.


8. Feature Module: Markdown Studio

Files: src/components/studios.tsxMarkdownStudio, src/routes/markdown.tsx.

Data flow

Textarea ──> source (usePersistedState "ss-md-source")
             │
             ▼
      marked.parse (sync)  ──> DOMPurify.sanitize  ──> html
             │
             ▼
   previewRef.innerHTML = html  ──> renderMermaidIn(container, theme)

Features

Persistence keys


9. Feature Module: AVSC Viewer (Schema Studio)

Files: src/components/studios.tsxAvroSchemaStudio, src/routes/avsc-viewer.tsx.

Validation pipeline (compileAvro)

  1. JSON.parse(text) → error surfaced if invalid.
  2. Root must be an object.
  3. type === "record".
  4. name must be a non-empty string.
  5. fields must be an array; every field must have name and type.

Returns either:

Preview

Exports

Persistence keys


10. Feature Module: Avro Viewer (Data Studio)

Files: src/components/studios.tsxAvroDataStudio, src/routes/avro-viewer.tsx.

High-level pipeline

sequenceDiagram
  participant U as User
  participant UI as AvroDataStudio
  participant W as "AvroWorker (Web Worker)"
  participant IDB as "IndexedDB (chunks store)"

  U->>UI: Choose .avro file
  UI->>IDB: resetAvroStore
  UI->>W: postMessage decode + transferred buffer
  W-->>UI: schema + codec + columns
  loop per OCF block
    W-->>UI: batch of rows
    UI->>UI: buffer rows and enqueue CHUNK_SIZE slice
    UI->>IDB: putChunks batch (coalesced writes)
    W-->>UI: progress decoded count
  end
  W-->>UI: done
  UI->>IDB: getChunk 0
  IDB-->>UI: pageRows
  UI-->>U: render Records table (page 1)

UI layout

  1. Stat cardsRecords, Schema fields, Columns, Codec. Records/fields/columns are recomputed by effectiveStats: if the file has a single wrapper record with one array-of-records field (plus scalar metadata like totalRecordCount), the stats reflect the inner dataset (fixes the common “Records: 1” wrap pattern).
  2. Schema pane (left, 35%): raw schema JSON pretty-printed, or an upload CTA if empty.
  3. Records pane (right, 65%): sticky-header table with:
    • null values italicized/muted.
    • Nested objects/arrays rendered by <JsonTree> (default depth 1).
    • status-like columns rendered as color-coded <StatusPill> (green/rose/amber/muted).
    • Optionally humanized cells for date/timestamp-looking columns.
  4. Pager« First, ‹ Prev, Next ›, Last » with page X / Y and row range rows a–b of total.
  5. Records options menu — toggle humanize, and export as JSON or CSV.
  6. Fullscreen — same fixed-inset pattern as Markdown.

Humanization (humanizeValue)

Applies only to non-object cells whose column name matches heuristics:

Single-record view

When meta.total === 1, the records pane switches to <RecordView> — record-array fields become titled sub-tables (rendered via <JsonTree> with table detection), and scalar fields appear as key: value lines below.

Exports

Persistence keys


11. Avro Decoder Internals

File: src/lib/avro.ts.

Pure TypeScript, dependency-free. Supports Avro Object Container File (OCF) with:

Reader

OCF layout enforced

Streaming variant

decodeAvroContainerStreaming(bytes, onSchema, onBlock) emits the schema exactly once, then invokes onBlock(records) per block. Used by the Web Worker to keep memory bounded.

Normalization helpers


12. Large-Dataset Pipeline (Worker + IndexedDB)

Worker (src/lib/avro-worker.ts)

main → { type: "decode", buffer } (transferred ArrayBuffer)
worker → { type: "schema", schema, codec, columns }
       ↺ { type: "batch", rows, startIndex }
       ↺ { type: "progress", decoded }
worker → { type: "done", total }  |  { type: "error", message }

Chunk store (src/lib/avro-store.ts)

Backpressure model

The Studio’s drain() runs at most one IDB transaction at a time; new batches queue while a drain is in flight. This keeps memory bounded even if the worker outpaces IDB throughput.


13. Sharing Subsystem

Applies only to Markdown documents.

Data model — public.shares

Column Type Notes
id UUID PK gen_random_uuid()
share_id TEXT unique 9-char lowercase base-32-ish (abcdefghijkmnopqrstuvwxyz23456789)
owner_id UUID FK → auth.users(id)
title TEXT default Untitled
content TEXT Markdown source
permission TEXT 'view' \| 'edit' (CHECK)
password_hash TEXT nullable
created_at, updated_at TIMESTAMPTZ trigger auto-updates updated_at

RLS — SELECT/INSERT/UPDATE/DELETE all gated by auth.uid() = owner_id. Public reads use a server-side REST call with the service/publishable key so unauthenticated visitors can read the row without RLS bypass in the client. Realtime publication includes shares with REPLICA IDENTITY FULL.

Server functions (src/lib/share.functions.ts)

Function Method Auth Purpose
createShare POST Insert new row with retry on 23505 (unique collision)
listMyShares GET List all owner shares
updateShareSettings POST Change title/permission/password (null removes, undefined keeps)
deleteShare POST Delete by share_id scoped by owner_id
getShareMeta GET Returns metadata only; no content
getShareContent POST Returns content, or password_required / wrong_password / not_found
updateSharedContent POST Public-writer path for edit permission; requires correct password if set

Authenticated fns compose requireSupabaseAuth middleware; unauthenticated fns hit Supabase REST directly using env keys (SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY / SUPABASE_PUBLISHABLE_KEY / etc.).

Password hashing

Share creation UX (ShareButton + ShareDialog)

Public share page (routes/s.$shareId.tsx)

Owner management (routes/_authenticated/my-shares.tsx)


14. Authentication

src/routes/auth.tsx — a single card with:

src/hooks/use-auth.tsxAuthProvider subscribes to supabase.auth.onAuthStateChange and hydrates via getSession().

Auth attachment for server fns


15. Persistence & Storage

Layer Purpose Scope
localStorage (usePersistedState) Markdown source & filename, AVSC source & filename, humanize toggle, forge-theme Persists across sessions
localStorage reset on mount Clears ss-avsc-*, ss-avro-mode, ss-avro-humanize, ss-section Per shell mount
IndexedDB (schema-studio-avro) Avro row chunks (paging) Reset on shell mount and per import
Supabase Postgres Shared markdown documents Owner-only, with public read via server fn

16. Theming

Tailwind v4 is used via the shadcn/ui component set. Utility patterns like bg-gradient-to-br from-primary/20 to-primary/5 drive the stat cards.


17. Server & SSR


18. Security Considerations


19. Performance Characteristics


20. Error Handling & Observability


21. Configuration & Environment

Required environment variables:

Variable Purpose
VITE_SUPABASE_URL / SUPABASE_URL Supabase project URL
VITE_SUPABASE_PUBLISHABLE_KEY / SUPABASE_PUBLISHABLE_KEY Client-safe API key
SUPABASE_SERVICE_ROLE_KEY (or fallbacks) Used by public-read server fns for share content

vite.config.ts delegates env injection to @lovable.dev/vite-tanstack-config (auto-injects VITE_*).


22. Build, Dev, and Deployment

Package scripts:

Script Purpose
npm run dev Vite dev server (SSR + HMR)
npm run build Production build via Vite + Nitro
npm run build:dev Development-mode build
npm run preview Preview built site
npm run lint ESLint
npm run format Prettier

Target runtime — Nitro builds a Cloudflare Worker (workerd-compatible). The share-functions module explicitly uses WebCrypto (crypto.subtle) so it runs unmodified on workerd.

Lovable integration — the repo is connected to Lovable; commits pushed to the connected branch sync back into the Lovable editor. See AGENTS.md.


23. Database Schema (Supabase)

File: supabase/migrations/20260723170117_d547e1ef-bfac-4bc9-8af0-65d13ee54df5.sql.

CREATE TABLE public.shares (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  share_id TEXT UNIQUE NOT NULL,
  owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  title TEXT NOT NULL DEFAULT 'Untitled',
  content TEXT NOT NULL DEFAULT '',
  permission TEXT NOT NULL DEFAULT 'view' CHECK (permission IN ('view','edit')),
  password_hash TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Highlights:


24. Extensibility Notes


Crafted with ♥ by Sanidhya Dash.