Custom views

1. Architecture & Where Custom Views Live

Custom Views (custom_views) are interactive HTML + Tailwind CSS + JavaScript micro-applications, dashboards, forms, booking engines, client portals, and landing pages that run inside a hardened <iframe sandbox> (SandboxedViewFrame) connected to Satia's database via an asynchronous postMessage IPC bridge (window.satia).

1.1 Pre-Injected Sandbox Environment (SandboxedViewFrame)

Your code_content is injected directly inside <div id="satia-app-root">...</div> of an already-initialized HTML document. The host <head> automatically pre-loads:

  • Tailwind CSS CDN (https://cdn.tailwindcss.com) configured with Shadcn HSL semantic variables (background, foreground, card, primary, secondary, muted, accent, destructive, border, input, ring).
  • Google Fonts: Plus Jakarta Sans (font-sans) and JetBrains Mono (font-mono).
  • Lucide Icons (https://unpkg.com/lucide@latest): Use <i data-lucide="icon-name" class="w-4 h-4"></i>. If you render icons dynamically via JavaScript, call if (window.lucide) window.lucide.createIcons(); after updating the DOM.
  • Built-in Utility Classes: .satia-card, .satia-btn, .satia-btn-primary, .satia-btn-secondary, .satia-btn-outline, .satia-btn-ghost, .satia-btn-destructive, .satia-input, .satia-badge, and .custom-scrollbar.
  • Optional Libraries: Do NOT re-import Tailwind or Lucide. Only import <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> when building dashboards with canvas charts.

1.2 The 5 Rendering Contexts & Routes (Web + Mobile App)

Every Custom View you create in custom_views works automatically both in the Satia Web Platform and inside the Satia Mobile App (iOS & Android) under Mis Apps (Custom Views), allowing businesses to build their own custom mobile applications (e.g., warehouse barcode scanners, field delivery trackers, POS terminals, or inspection checklists) without publishing a separate mobile app:

  1. Standalone Micro-App (entity_slug = None):
    • Appears in the workspace sidebar under custom_views.module (e.g., "Ventas", "Operaciones") on Web, and in the Custom Views (Mis Apps) launcher on the Satia Mobile App.
    • Internal Route: /views/[slug] -> markdown link: [View Name](/views/my-slug).
  2. Table-Bound View (entity_slug = '<table_slug>'):
    • Appears as an interactive view tab directly inside that business table's header (both on Web and Mobile).
    • Internal Route: /erp/[entity_slug] -> markdown link: [Table Name](/erp/pedidos).
  3. Public Shareable Link (shared_view_links):
    • Accessible by external clients, suppliers, or guests on any desktop or mobile browser without requiring a Satia staff account.
    • Public Route: /shared/view/[token] -> markdown link: [Open Link](/shared/view/abc123token).
    • Contextual Signed Link (?ctx=): /shared/view/[token]?ctx=<signed_hmac_token> binds the session to a specific contact (window.satia.context.contact).
  4. External & Internal Embed Mode (/shared/view/[token]?embed=1):
    • Designed for embedding inside external websites (WordPress, Webflow, Shopify) or parent landing pages:
      • Sets html, body { background: transparent !important; }, hides the floating Satia badge and loading overlay, and enables allow-popups allow-popups-to-escape-sandbox.
      • Auto-Resize (ResizeObserver): Automatically observes #satia-app-root and posts height changes to the parent window.
      • URL Query Parameters (window.satia.params): Any URL query parameters passed to the embed URL (excluding ctx and embed, up to 20 alphanumeric/_ keys of max 500 chars, e.g., ?embed=1&utm_source=meta&plan=pro&landing_path=/promo) are automatically injected into window.satia.params.
      • Parent Window PostMessage Protocol (SATIA_EMBED): The parent page receives structured messages from the embedded view:
        • { source: 'SATIA_EMBED', token: '<token>', type: 'ready' } (fired once Tailwind styles settle and initial height is > 0).
        • { source: 'SATIA_EMBED', token: '<token>', type: 'resize', height: <number> }.
        • { source: 'SATIA_EMBED', token: '<token>', type: 'event', event: '<eventName>', data: <any> } (triggered whenever the view calls window.satia.emit(eventName, data)).
        • { source: 'SATIA_EMBED', token: '<token>', type: 'error', message: '<string>' }.
  5. Satia Mobile App Execution (window.satia.env.platform === 'mobile'):
    • When opened inside the Satia Mobile App (iOS/Android), the view runs as a native-feeling mobile app with direct access to phone hardware via window.satia.native (camera QR/barcode scanner, GPS coordinates, biometric FaceID/TouchID, and haptic feedback — see Section 3.8).

2. Builder Tools & Surgical Editing Workflow

2.1 Composable Knowledge Tree (discovery__get_ui_design_manual — 6 Specialized Branches)

This manual (03_custom_views_and_design.md) is the Root Trunk: it defines how all Custom Views are saved, secured (data_binding), connected to business tables (getEntities / createEntity / updateEntity / deleteEntity), and styled under the Satia Design System.

Before writing code_content, unlock the specific knowledge branches your view needs by calling discovery__get_ui_design_manual. You can request one branch (category='...') or combine multiple branches in a single call (categories=['...', '...']):

Branch (category)What It UnlocksCombine When...
'dashboards'4-Layer KPI & Chart architecture (At-a-Glance KPIs → Trends → Top-N Breakdowns → Evidence Table), Chart.js configuration, anti-rainbow rules, and global filter synchronization.Building executive panels, analytics, financial monitors, or operational boards.
'forms'Single-page (≤ 6 fields) vs. genuine multi-step wizard (≥ 7 fields), interactive Choice Cards instead of native <select>, Cal.com-style Date & Time-Slot Pill Grids, Media Dropzones (uploadMedia), inline validation, and Receipt state.Building booking engines, checkouts, data capture, or request wizards.
'landing_pages'Editorial / Swiss / Warm Craft / Bold Poster art directions (Light Mode default), "One Live Thing" interactive artifact grounded in real ERP data, and strict bans on AI clichés.Building public landing pages, digital menus, catalogs, storefronts, or client portals.
'auth'Deep-dive on window.satia.auth: choosing between Passwordless Lead Gate (identify), Full Account Auth (login / register / logout), or Contextual ?ctx= links; onAuthChange reactive lifecycle; hot session migration; and auth UI/UX patterns.The view requires client login, registration, participant accounts, or a passwordless lead gate.
'embedded_agents'Deep-dive on window.satia.agent (channel_type = 'web'): data_binding.agent_id, init, sendMessage, setContext (live screen context injection), onMessage (msg.role === 'model'), onStatus, auth_mode, welcome_message, multimodal media rendering (image, pdf, audio, video), and Standalone Chat vs. Contextual Copilot UX patterns.Embedding a transactional AI agent as a full chat app, split-screen assistant, or floating copilot.
'mobile'Deep-dive on building mobile-first field/operations apps for the Satia Mobile App (window.satia.env.platform === 'mobile') and window.satia.native (scanBarcode, getLocation, authenticateBiometric, haptic), thumb-zone sticky bars, and web fallbacks.Building POS terminals, barcode/QR scanners, field delivery trackers, check-in kiosks, or mobile-first tools.
# Example: A public storefront with an embedded AI shopping copilot and a lead gate
discovery__get_ui_design_manual(categories=["landing_pages", "embedded_agents", "auth"])

# Example: A mobile warehouse scanner & inventory form
discovery__get_ui_design_manual(categories=["forms", "mobile"])

2.2 Saving & Updating Custom Views (database__insert & database__update on custom_views)

You create and update views directly on the custom_views table using database__insert and database__update.

  • Automatic Upsert by slug: Calling database__insert(table_name="custom_views", data={...}) automatically checks if a view with that slug already exists in the workspace; if it does, it updates the existing view transparently.
  • Virtual table_slug Resolution: Pass "table_slug": "pedidos" (or omit it for standalone sidebar views) to bind the view as an interactive tab inside that business table (/erp/pedidos).
database__insert(
    table_name="custom_views",
    data={
        "name": "Panel Ejecutivo de Ventas",
        "slug": "panel-ejecutivo-ventas",       # Unique lowercase slug with hyphens (/views/[slug])
        "code_content": "<div>...</div>",       # Self-contained HTML + Tailwind + <script>
        "view_type": "dashboard",               # 'dashboard' | 'form' | 'card_grid' | 'custom'
        "module": "Ventas",                     # Sidebar module name (for standalone views)
        "table_slug": "pedidos",                # Optional: bind as a tab inside a business table slug
        "description": "Executive dashboard for revenue, funnel, and top products.",
        "icon": "BarChart3",                    # Lucide icon name
        "data_binding": {
            "target_entity_slug": "pedidos",
            "allowed_actions": ["read", "create", "update"],
            "writable_fields": ["estado", "notas", "comprobante"],
            "readable_fields": ["*"],
            "default_filters": {},
            "require_login": False,
            "tables": {
                "pedidos": {
                    "permission": "write",
                    "isolation": "auto",
                    "column_permissions": {"costo_interno": "hidden"},
                    "default_filters": {"estado": "neq.borrador"}
                },
                "productos": {
                    "permission": "read",
                    "isolation": "none"
                }
            }
        }
    }
)

2.3 Creating Public Share Links (database__insert on shared_view_links)

Create public links by inserting into shared_view_links. You can pass either custom_view_id (UUID) or view_slug (the view's slug, which is auto-resolved to the view's UUID), and token is generated automatically if omitted:

database__insert(
    table_name="shared_view_links",
    data={
        "view_slug": "panel-ejecutivo-ventas",  # Or "custom_view_id": "<VIEW_UUID>"
        "permission": "write",                  # 'read' (dashboards/catalogs) or 'write' (forms/portals/POS)
        "label": "Public booking link",
        "filter_override": {"estado": "activo"} # Optional server-side PostgREST filter override
    }
)

Returns token and url_path (/shared/view/<token>).

2.4 Surgical Editing of Existing Views (database__inspect_text + database__patch_text)

When fixing a bug, adjusting styles, or adding features to an existing view in custom_views:

  1. Inspect in Broad Windows (150–300 lines): Use database__inspect_text(table_name="custom_views", record_id="<view_id>", column_name="code_content", search_pattern="...") to locate anchors, and read broad slices (150–300 lines per call rather than tiny 20-line slices) so you have full structural context in a minimal number of tool calls.
  2. Batch Edits in a Single database__patch_text Call: Group all related edits into a single database__patch_text(..., replacements=[...]) call instead of making separate top-to-bottom patch_text calls. The engine automatically compensates for line-number shifts (line_delta) across multiple replacements in the same call.

3. Complete window.satia JavaScript SDK Reference

The bridge auto-initializes on DOMContentLoaded, retries handshakes if needed, and queues any early requests until INIT completes. Calling await window.satia.init() at the start of your main async function is safe and recommended.

3.1 Record Normalization (normalizeEntityRow) & Automatic Hydration (*_resolved)

Every record returned by getEntities, createEntity, updateEntity, auth.login, and auth.register is automatically normalized and hydrated:

  1. Dual Access (Root + attributes): All dynamic column keys are flattened directly onto the root object (row.estado, row.precio, row.cliente_id) AND preserved inside row.attributes (row.attributes.estado). Native columns (id, name, starts_at, ends_at, created_at, updated_at) take priority at the root when non-null.
  2. Automatic Relation, Tag & Knowledge Hydration (<key>_resolved):
    • relation columns: If a record has cliente_id: "uuid", the engine automatically populates row.cliente_id_resolved (and row.attributes.cliente_id_resolved) with { id, name, type } (or an array of { id, name, type } for multi-relations). Always render row.cliente_id_resolved?.name || 'Unassigned' in UI tables and cards—never display raw UUIDs!
    • tags columns: Populates row.<tags_key>_resolved with an array of { id, name, color, type: 'tag' }.
    • knowledge columns: Populates row.<knowledge_key>_resolved with { id, name, title, doc_type, url, file_url, filename, mime_type, size, ai_description, content, metadata }.
  3. Automatic Cart / Line Items Hydration (line_items):
    • On event tables, each line_items column (e.g., productos_seleccionados) is automatically hydrated from event_items as an array of item objects directly at row.productos_seleccionados (and row.attributes.productos_seleccionados).

3.2 Core Data & Schema Methods

  • await window.satia.init() Initializes the bridge and returns { status: 'connected', session: window.satia.context, params: window.satia.params }.
  • await window.satia.getEntities(entitySlug, filters = {}) Fetches records for entitySlug ordered by created_at DESC.
    • Important Filter Rule: In getEntities(slug, filters), the client-side filters object supports { filter_by_current_contact: true } (which filters server-side by the authenticated contact_id in window.satia.context.contact.id). For arbitrary UI search/category/date filtering, filter the returned array in JavaScript (rows.filter(...)), or configure permanent server-side filters in data_binding.default_filters / data_binding.tables[slug].default_filters / shared_view_links.filter_override.
    • Satia Docs Shortcut: Calling getEntities('knowledge_docs' | 'archivos' | 'recursos' | 'files' | 'assets', filters) automatically routes to getAssets(filters).
  • await window.satia.getSchema(entitySlug?) Returns { id, name, slug, type, module, icon, description, attributes: [{ id, key, label, type, is_required, is_unique, config, sort_order }] } (filtered by readable_fields and excluding 'hidden' columns).
  • await window.satia.createEntity(entitySlugOrData, maybeData?) Supports both createEntity('pedidos', { ... }) and createEntity({ ... }) (uses target_entity_slug).
    • Automatic Contact Binding: If a contextual/authenticated session (window.satia.context.contact.id) is active, creating an event record automatically sets contact_id, cliente: [contactId], and contact: [contactId].
    • Smart Contact Deduplication: If createEntity targets a contact table while a contact session (contact_id) is active, the backend updates/merges the existing contact record instead of creating a duplicate!
    • Automatic Schedule Calculation: Passing an ISO string or { start, end } to a schedule attribute automatically populates physical starts_at and ends_at (calculating ends_at from default_duration_minutes when configured).
    • Automatic Line Items Creation: Passing an array of cart items ([{ offering_id, name, unit_price, quantity, attributes }]) in a line_items attribute key automatically inserts the rows into event_items with the corresponding attribute_key.
    • Meta Lead Tracking: Submitting a form on a public link automatically triggers trackSatiaFormLead for conversion tracking.
  • await window.satia.updateEntity(entitySlugOrId, idOrData, maybeData?) Supports both updateEntity('pedidos', recordId, { estado: 'pagado' }) and updateEntity(recordId, { estado: 'pagado' }).
    • Performs a safe server-side JSONB merge on attributes (never overwrites unspecified fields).
    • If a line_items array is included in the update payload, it atomically replaces the event_items for that attribute_key.
  • await window.satia.deleteEntity(entitySlugOrId, maybeId?) Supports both deleteEntity('pedidos', recordId) and deleteEntity(recordId).
    • Public Link Guardrail: On public links (shared_view_links), deleteEntity is blocked by default unless 'delete' is explicitly included in data_binding.allowed_actions.

3.3 Media & Satia Docs Assets Methods

  • await window.satia.uploadMedia(fileInputOrFile, entitySlug = 'general', attributeKey = 'media') Accepts a <input type="file"> DOM element, a FileList, or a File/Blob object directly. Serializes the binary via ArrayBuffer over postMessage, uploads it to Supabase Storage (satia-media), and returns a MediaItem: { url: "https://...", name: "recibo.jpg", type: "image" | "video" | "audio" | "document", mime_type: "image/jpeg", size: 12345 }. Save [mediaItem] (array) or mediaItem into your table's media column via createEntity / updateEntity. (Note: On public links, requires permission: 'write' and, if writable_fields is non-empty, attributeKey must be listed in writable_fields).
  • window.satia.getMediaUrl(mediaAttr, fallback = '') Safely extracts the first valid URL from any media or knowledge_resolved attribute (whether it is a MediaItem array, a single object, or a direct URL string) and resolves relative paths.
  • await window.satia.getAssets(filter = {}) Queries standalone files (doc_type = 'file') from Satia Docs (knowledge_docs). Supports filter: { search: 'logo', folder_id: '...', tag: '...', mime_type: 'image/' }. Returns normalized asset objects with { id, title, name, url, file_url, filename, mime_type, size, ai_description, tags, tags_resolved, folder_id }.
  • await window.satia.getAssetUrl(nameOrTag, fallback = '') Smart asset resolver with in-memory caching: searches Satia Docs standalone files by exact ID, substring match in title, filename, tags, or ai_description, or word-token match. If querying 'satia' or 'logo' with no custom brand logo uploaded, automatically falls back to the host's /SATIA.svg (or /SATIA_blanco.svg if 'blanco'/'white'/'dark' is in the query). (Bonus: The sandbox also runs a MutationObserver and <img> error listener that automatically resolves relative or broken <img> src attributes against Satia Docs assets).

3.4 Native End-User Authentication & Lead Gates (window.satia.auth) — Unlock category='auth'

MANDATORY RULE: Whenever a Custom View requires login, registration, participant accounts, or passwordless lead identification, ALWAYS use window.satia.auth and call discovery__get_ui_design_manual(category="auth") (or include "auth" in categories=[...]) for the complete contract and UI/UX patterns. NEVER build DIY login lookups using getEntities() / createEntity().

  • Core Methods Overview:
    • await window.satia.auth.identify({ name, phone?, email?, entitySlug?, ...customAttrs }) — Passwordless Lead Gate (captures/merges contact and issues a 24h signed session).
    • await window.satia.auth.register({ identifier, password, name?, entitySlug?, ...customAttrs }) — Creates a scrypt-hashed account (or merges seamlessly if identifier + password match).
    • await window.satia.auth.login({ identifier, password, entitySlug? }) — Verifies credentials and persists a 24h HMAC-SHA256 session in localStorage.
    • await window.satia.auth.logout() — Clears session and fires satia:auth-changed.
    • window.satia.auth.getUser(), window.satia.auth.getSession(), window.satia.auth.isAuthenticated(), window.satia.auth.onAuthChange(cb) — Session inspection and reactive UI state listeners.

3.5 Business Timezone Date Helpers (window.satia.dates)

All dates are formatted in the workspace's configured IANA timezone (window.satia.dates.timezone, e.g., "America/Bogota"):

  • window.satia.dates.getToday() -> "YYYY-MM-DD" in business local time.
  • window.satia.dates.toUTC(dateStr, timeStr = '12:00') -> Converts local "YYYY-MM-DD" + "HH:MM" to an ISO 8601 UTC string ("2026-04-15T14:00:00.000Z") ready to save in schedule / starts_at.
  • window.satia.dates.format(date, options?) -> Localized human date string (es-CO).
  • window.satia.dates.formatTime(date, hour12 = true) -> Localized time string (e.g., "9:30 a. m.").
  • window.satia.dates.extract(dateOrRecord) -> Inspects an ISO string or a record (starts_at, fecha_de_reserva, fecha, created_at) and returns { ymd, timeStr, hour, minute, hourDecimal, dateObj } in business local time.

3.6 Shopping Cart & Currency Helpers (window.satia.cart)

  • window.satia.cart.calculateTotal(items) -> Sums Number(item.unit_price ?? 0) * Number(item.quantity ?? 1).
  • window.satia.cart.calculateCount(items) -> Sums Number(item.quantity ?? 1).
  • window.satia.cart.formatItem(item, options?) -> Normalizes { offering_id, name, unit_price, quantity, attributes }.
  • window.satia.cart.formatCurrency(amount, currency?, locale?) -> Formats numbers/currencies using Intl.NumberFormat.

3.7 UI Feedback, Modals & Embed Events

  • window.satia.toast(message, type = 'info'): Displays a native toast notification in the host app ('success' | 'error' | 'info' | 'warning'). Never use browser alert()!
  • window.satia.getParam(key, fallback = ''): Reads a URL query parameter from window.satia.params (e.g., const utm = window.satia.getParam('utm_source', 'direct')).
  • window.satia.emit(eventName, data = {}): Emits a custom event to the parent host or embedding page (SATIA_EMBED).
  • DOM Helpers: window.satia.openModal(id), window.satia.closeModal(id), window.satia.selectTab(tabGroupId, activeTabId), window.satia.toggleAccordion(id).

3.8 Satia Mobile App Capabilities (window.satia.native) — Unlock category='mobile'

When opened inside the Satia Mobile App (iOS & Android), window.satia.env.platform === 'mobile' is set and window.satia.native exposes phone hardware (scanBarcode(), getLocation(), authenticateBiometric(reason), haptic(style)).

  • Mandatory Discovery: Whenever building POS terminals, barcode/QR scanners, field delivery trackers, on-site check-in kiosks, or mobile-focused views, call discovery__get_ui_design_manual(category="mobile") (or include "mobile" in categories=[...]) for the complete hardware bridge patterns, thumb-zone ergonomics, and mandatory web fallbacks.

3.9 Embedded Transactional Agents & Web Chat (window.satia.agent) — Unlock category='embedded_agents'

You can embed any transactional agent (type = 'transactional') directly inside a Custom View over the dedicated channel_type = 'web' channel—without writing any automation_scripts (on_inbound / on_outbound):

  • Mandatory Discovery: Whenever a view embeds a conversational agent or AI copilot, call discovery__get_ui_design_manual(category="embedded_agents") (or include "embedded_agents" in categories=[...]) for the full contract (data_binding.agent_id, init, sendMessage, setContext, onMessage with strict msg.role === 'model', onStatus, cancel, reset, auth_mode, welcome_message, and multimodal media rendering).

4. Multi-Layer Security, data_binding & PII Shield

Public views (/shared/view/[token]) enforce strict server-side security in actions.ts:

4.1 Table Scope Validation (isEntityAllowedInPublicView)

A public view can ONLY query or mutate a business table if that table's slug is authorized by at least one of:

  1. The view's bound entity_slug or data_binding.target_entity_slug.
  2. An entry in data_binding.tables[slug].
  3. A literal call in code_content (e.g., window.satia.getEntities('productos')). Arbitrary requests to unreferenced tables are rejected by the server.

4.2 Per-Table Policies (data_binding.tables) & Isolation ('auto' vs 'none')

Each table in data_binding.tables supports:

{
  "tables": {
    "citas": {
      "permission": "write",
      "isolation": "auto",
      "column_permissions": {
        "notas_internas": "hidden",
        "precio": "read",
        "fecha": "write"
      },
      "default_filters": {
        "estado": "neq.cancelada"
      }
    }
  }
}
  • isolation: 'auto' (Default):
    • On update, update_status, and delete, requires an authenticated contact session (contact_id) and verifies that the record belongs to that contact_id (row.id === contact_id for a contact table, or attributes.contact_id / cliente / contact matches contact_id for an event table).
  • isolation: 'none': Allows public read/write according to permission and column_permissions without requiring row ownership (e.g., public catalogs, open check-in boards).
  • require_login: true (at root of data_binding): Blocks getEntities (returns []) and all mutations until the user logs in via window.satia.auth or opens a signed ?ctx= link.
  • column_permissions: Map of column_key -> 'hidden' | 'read' | 'write'. 'hidden' strips the field (and <key>_resolved) from getEntities and getSchema; 'read' strips the field from createEntity and updateEntity payloads.
  • Dangerous Keys Blacklist: The server always strips password, password_hash, and platform_identifiers from all public reads and writes.

4.3 Automatic Contact PII Shield on Public Links

When a public link queries a contact table (e.g., a tournament leaderboard or participant directory), the server automatically strips sensitive PII (phone, telefono, whatsapp, celular, movil, email, correo) from all rows that do NOT belong to the currently logged-in contact (row.id !== resolvedContactId), UNLESS you explicitly whitelist that field in column_permissions ({"telefono": "read"}) or readable_fields (["name", "telefono"]).

4.4 Server-Side Filter Syntax (default_filters & filter_override)

Both data_binding.default_filters, data_binding.tables[slug].default_filters, and shared_view_links.filter_override execute server-side in PostgreSQL before data ever reaches the browser:

  • Supports PostgREST operators: eq.<val>, neq.<val>, gt.<val>, gte.<val>, lt.<val>, lte.<val>, like.<val>, ilike.<val>, in.<val1,val2>. (Plain values default to eq).
  • Supports the dynamic session placeholder {{contact_id}}, which is replaced at runtime with the verified contact_id from ?ctx= or window.satia.auth (e.g., {"cliente_id": "eq.{{contact_id}}"}).

5. Contextual Signed Links (?ctx=) for Transactional Agents

Transactional agents (on WhatsApp, Instagram, Web, etc.) can generate personalized, cryptographically signed links (HMAC-SHA256, 24-hour expiration) for the contact they are currently chatting with using the store tool store_custom_views__generate_link:

  • How to enable it for an agent: Install the Custom Views store tool (tool_key = 'custom_views') on the transactional agent via store__install_tool.
  • Tool Parameters (store_custom_views__generate_link):
    • view_slug (required): Exact slug of the target Custom View.
    • prefill_data (optional JSON string or dict): Key-value pairs to pre-populate in the view (exposed at window.satia.context.prefill).
    • permission (optional): 'write' (default) or 'read'.
  • What happens when the client opens /shared/view/[token]?ctx=<hmac>:
    1. window.satia.context.contact is pre-populated with { id, name, phone, attributes }.
    2. window.satia.context.prefill contains the prefill_data passed by the agent.
    3. Any record created via window.satia.createEntity is automatically linked to contact.id (and if the form updates the contact table, it merges into the existing contact without creating duplicates).

6. Official Satia Design System ("Black and white is the brand. Color is the answer.")

Every Custom View built for Satia must follow the official brand Design System unless the user explicitly asks to use their own company logo colors.

6.1 The 90 / 10 Proportion (60% White · 30% Black · 10% Color)

  • 60% White Card (#FFFFFF): Primary page background and base containers.
  • 30% Black Card (#000000): High-impact hero cards, featured headers, primary buttons, and focus panels.
  • Structural Neutrals (Only 3 Grays Allowed):
    • satia-gris-linea: #E8E8E8 — The only color for 1px solid #E8E8E8 dividing borders.
    • satia-gris-fondo: #F5F5F5 — Secondary module backgrounds, input fills, or light hover states.
    • satia-texto-apagado: #A3A3A3 — Secondary metadata, subtitles, or placeholders.
  • Strict Prohibitions:
    • ❌ Zero box-shadow (drop-shadows) on cards or buttons. Spatial separation is achieved via 1px solid #E8E8E8 borders or pure #FFFFFF vs. #000000 contrast.
    • ❌ Zero "soft blacks" or bluish grays (#050811, slate-900, gray-800). Satia black is pure #000000.
    • ❌ Zero mid-gray body text (#555, #666). Body text is #000000 on white or #FFFFFF on black.

6.2 The 9 Official Secondary Brand Colors (In Strict Brand Sequence)

The 10% of color lives in status badges, charts, indicators, avatars, and interactive accents. When displaying multiple series, badges, or categories, always follow this exact sequence:

OrderOfficial NameTokenHexPrimary Semantic Usage
1Jade--satia-jade#00CFB5Success, active, completed, primary series #1
2Cenote--satia-cenote#08C6D2Information, sync, speed, series #2
3Talavera--satia-talavera#4381CFLinks, financial analytics, series #3
4Jacaranda--satia-jacaranda#8B6FC9AI, agents, automation, multipliers, series #4
5Bugambilia--satia-bugambilia#ED71BECreative highlights, marketing, series #5
6Grana--satia-grana#E04264Critical error, cancelled, urgent, red alert, series #6
7Cempasúchil--satia-cempasuchil#FF993EWarning, pending review, series #7
8Maíz--satia-maiz#FFD949In progress, waiting, star/reward, series #8
9Limón--satia-limon#D3E736New feature, growth, high performance, series #9

Text Contrast Rule on the 9 Brand Colors: On top of any of the 9 secondary colors, text and icons are always pure Black #000000 (never white).

6.3 Official Typography (Onest + Plus Jakarta Sans)

Always include the two official fonts from Google Fonts in the <head> of your Custom Views:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Onest:wght@500;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
  • Onest (font-display): Used for all headings (h1–h3, weight 700, letter-spacing: -0.03em) and large numeric KPIs (font-weight: 800, font-variant-numeric: tabular-nums).
  • Plus Jakarta Sans (font-body): Used for body copy (15px), tables, forms, labels (13px, weight 600), and buttons.
  • Sentence case always: Write "Orders of the day" or "Pedidos del día" instead of "ORDERS OF THE DAY". Avoid all-caps with wide letter-spacing.
  • Pill Buttons (border-radius: 9999px / rounded-full):
    • Primary on white surface: background #000000, text #FFFFFF, padding: 10px 20px, font-weight: 600.
    • Primary on black card: background #FFFFFF, text #000000 (or #00CFB5 Jade background with #000000 text).
    • Secondary: background #F5F5F5, text #000000, border 1px solid #E8E8E8.

7. Official Sidebar Icons (SATIA_ICONS)

When inserting or updating custom_views.icon, choose an icon name from the official Lucide catalog supported by the Satia Sidebar:

  • Operations & Commerce: ShoppingBag, ShoppingCart, Package, Truck, Store, Utensils, Coffee, Receipt, CreditCard, DollarSign, Wallet, Tag, Barcode, Boxes.
  • People & CRM: Users, UserCheck, UserPlus, Contact, HeartHandshake, Building2, Briefcase, Award, Stethoscope, GraduationCap.
  • Calendar & Tasks: Calendar, CalendarClock, Clock, CheckSquare, ListTodo, ClipboardList, Kanban, Timer, Bell.
  • Analytics & Dashboards: LayoutDashboard, BarChart3, LineChart, PieChart, TrendingUp, Activity, Target, Gauge, Sparkles.
  • Documents & Media: FileText, FolderKanban, BookOpen, Image, Camera, QrCode, MapPin, Globe, Wrench, Settings.

8. Pre-Save Verification Checklist (custom_views)

  1. Table Inspection First: Did you call database__describe_tables on every table used by the view to verify exact column keys, status options, and relation targets?
  2. Complete Self-Contained HTML: Does code_content start with <!DOCTYPE html> and include Tailwind CDN + Onest and Plus Jakarta Sans fonts?
  3. data_binding Alignment: Does data_binding.tables authorize every business table slug referenced in window.satia.getEntities / createEntity / updateEntity / deleteEntity?
  4. Mobile & Web Ready: Are grids responsive (grid-cols-1 md:grid-cols-3) and are window.satia.native features guarded with graceful web fallbacks?
  5. Surgical Edits Only on Existing Views: If modifying an existing view, did you use database__inspect_text + database__patch_text instead of rewriting the entire code_content?