A starter Wippy application with user management, AI assistant, and admin frontend.
- Wippy CLI
- Node.js 18+
cp .env.example .env
make build
wippy run -cOpen http://localhost:8080. Default admin: admin@wippy.local / admin123.
# Build main app
cd frontend/applications/main && npm install && npm run build -- --outDir ../../../static/app/main --emptyOutDir
# Build a web component
cd frontend/web-components/reaction-bar && npm install && npm run build -- --outDir ../../../static/wc/reaction-bar --emptyOutDir
# Frontend watch mode (main app)
cd frontend/applications/main && npm run dev
# Server (separate terminal)
wippy run -cThe Web Host is a set of JS/CSS/HTML modules served from a CDN (e.g., https://web-host.wippy.ai/webcomponents-1.0.12/). It provides:
- Chat UI — conversation interface with AI agents
- Navigation — left sidebar with page links, user menu
- Page rendering — loads
view.pageentries as sandboxed iframes - Theme injection — injects CSS (fonts, theme variables, PrimeVue styles) into child iframes
- WebSocket relay — real-time event distribution to child content
The Web Host is the "shell" that wraps your application's frontend pages.
The facade (wippy/facade) is the backend module that bootstraps the Web Host. It's wired as a dependency in src/app/deps/_index.yaml and controls:
- Which Web Host version to load from CDN
- App title, icon, and branding
- Theme customization (colors, fonts, icons)
- Feature flags (navigation visibility, chat sidebar, etc.)
- Login path and session configuration
The facade serves a thin index.html at / that fetches config from the backend (GET /api/public/facade/config) and loads the Web Host bundle.
Host-rendered pages are registered as view.page entries in src/app/views/_index.yaml. The Web Host loads them in iframes and automatically injects CSS (theme, fonts, PrimeVue, custom overrides).
Non-host pages are served directly as static files (e.g., static/login.html). The Web Host does NOT render or inject anything into these — they must handle their own styling.
┌──────────────────────────────────────────────────────┐
│ Browser Window │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Web Host (from CDN) │ │
│ │ Loaded by facade's index.html at / │ │
│ │ │ │
│ │ ┌─────────┐ ┌────────────────────────────┐ │ │
│ │ │ Nav │ │ Page iframe (view.page) │ │ │
│ │ │ sidebar │ │ │ │ │
│ │ │ │ │ Your Vue app runs here │ │ │
│ │ │ - Pages │ │ with injected CSS + │ │ │
│ │ │ - Chat │ │ proxy API (window.$W) │ │ │
│ │ │ - User │ │ │ │ │
│ │ └─────────┘ └────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Non-host pages (login.html) load independently │
│ outside the Web Host — no CSS injection │
└──────────────────────────────────────────────────────┘
Facade (src/app/deps/_index.yaml)
│ Defines: custom_css, css_variables, icons, app_icon, app_title
│
▼
Web Host (CDN)
│ 1. Loads theme-config.css → defines --p-primary-*, --p-surface-*, --p-secondary-*
│ 2. Loads PrimeVue/Tailwind → components styled via those vars
│ 3. Applies custom_css → raw CSS string injected as <style>
│ 4. Applies css_variables → injected as :root { --key: value; } overrides
│ 5. Registers icons → custom Iconify collection under "custom:" prefix
│
▼
Child Iframe (view.page)
│ Host re-injects the same CSS into each iframe (controlled by proxy config):
│ - theme-config.css, PrimeVue CSS, iframe.css, fonts, custom_css, css_variables
│
✗ Non-host pages (login.html, etc.)
NOT reached by this flow — must be themed manually
All runtime configuration is driven by environment variables and the facade dependency parameters in src/app/deps/_index.yaml.
| Variable | Description | Default |
|---|---|---|
PUBLIC_API_URL |
Public-facing API URL | http://localhost:8080 |
ENCRYPTION_KEY |
Data encryption key (hex) | Auto-generated on first boot |
ANTHROPIC_API_KEY |
Claude API key for AI assistant | - |
The wippy/facade dependency controls the frontend shell:
| Parameter | Description | Default |
|---|---|---|
app_title |
Sidebar title | Wippy |
app_name |
Full app name | Wippy AI |
app_icon |
Iconify icon reference | wippy:logo |
show_admin |
Show admin controls | true |
hide_nav_bar |
Hide left sidebar | false |
start_nav_open |
Nav open on load | false |
allow_select_model |
User model selection | false |
disable_right_panel |
Disable right sidebar | false |
session_type |
non-persistent or persistent |
non-persistent |
history_mode |
hash or browser |
hash |
login_path |
Unauthenticated redirect | /login.html (this template overrides to /app/login.html) |
custom_css |
CSS injected into frontend | Poppins font import |
css_variables |
JSON object of CSS custom properties | {} |
icons |
JSON object of custom Iconify icons | {} |
CSS variables use PrimeVue naming: p-primary, p-primary-500, p-surface-0, p-text-color, etc.
src/app/ Backend (Wippy Lua)
api/ HTTP endpoints
agents/ AI agent definitions
users/ User management
security/ Access control
models/ LLM model config
views/ Frontend page registration
deps/ Module dependencies
env/ Environment storage
frontend/
applications/main/ Vue 3 admin panel (view.page)
web-components/
reaction-bar/ Emoji reaction buttons (example-reaction-bar)
websocket-log/ WebSocket message log (example-websocket-log)
chart-circle/ Chart.js doughnut chart (example-chart-circle)
mermaid/ Mermaid diagram renderer (example-mermaid)
markdown/ Markdown renderer (example-markdown)
model-gallery/ AI model card grid (example-model-gallery)
static/ Static assets (login page)
app/main/ Built main app (generated)
wc/*/ Built web components (generated)
The project includes 6 web component examples demonstrating different capabilities of the Wippy web component architecture.
Components are built on a two-layer base class system:
WippyElement(@wippy-fe/webcomponent-core) — Shadow DOM, host CSS loading, prop parsing from HTML attributes, event emissionWippyVueElement(@wippy-fe/webcomponent-vue) — Vue 3 app mounting, reactive props viauseComponentProps(), typed events viauseComponentEvents(), Pinia store support, children content viauseContent()
Each component is a self-contained ES module that registers a custom element tag. The host platform loads them dynamically and provides CSS assets (theme, fonts, PrimeVue) into each component's Shadow DOM.
| Component | Tag | Key Demo |
|---|---|---|
| reaction-bar | example-reaction-bar |
Event emission (useComponentEvents()), PrimeVue Button, Tailwind CSS |
| websocket-log | example-websocket-log |
WebSocket subscription via on() from proxy, theme-aware CSS |
| chart-circle | example-chart-circle |
Bundled 3rd-party lib (Chart.js), canvas rendering |
| mermaid | example-mermaid |
Bundled lib (beautiful-mermaid), CSS variable theming, children content |
| markdown | example-markdown |
markdown-it + sanitize-html, children content, markdownCssUrl |
| model-gallery | example-model-gallery |
Proxy API fetch (api.get()), PrimeVue Components (Button, Tag), Tailwind + PrimeUI |
Components can read content from child <template data-type="..."> elements. Enable via contentTemplate in the component config:
<example-mermaid>
<template data-type="text/vnd.mermaid">
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Do it]
B -->|No| D[Skip it]
</template>
</example-mermaid>
<example-markdown>
<template data-type="text/markdown">
# Hello World
This is **bold** and [a link](https://example.com).
</template>
</example-markdown>- No root-level padding or margin. Components must not add padding or margin to their outermost element. The consumer (host page, layout container, or parent component) controls all edge spacing.
- Inline styles for Shadow DOM. Tailwind classes work because they're compiled at build time into an inline CSS string injected as a
<style>element. Host CSS (fonts, theme, PrimeVue) is loaded asynchronously viahostCssKeys. - PrimeVue via
@wippy-fe/theme. ImportPrimeVuePluginfrom@wippy-fe/theme/primevue-plugin— it installs PrimeVue in unstyled mode ({ theme: 'none' }). Thetailwindcss-primeuiTailwind plugin (included in the theme preset) generates component styles. For web components, addPrimeVuePlugintovueConfig.pluginsand requestprimeVueCssUrlinhostCssKeysfor host-provided styles. - Theme-aware colors. Use semantic CSS variables (
--p-content-background,--p-text-color,--p-content-border-color,--p-text-muted-color) for colors. Never use numbered surface variables (--p-surface-0,--p-surface-100) for theme-dependent colors — they are a fixed light-to-dark scale that does not flip with dark mode. For derived shades, usecolor-mix():color-mix(in srgb, var(--p-content-background) 85%, var(--p-text-color) 15%). - Vite externals. Always externalize:
vue,pinia,@iconify/vue,@wippy-fe/proxy(provided by host import maps). Bundle everything else (chart.js, primevue, markdown-it, etc.). - ESLint. Each package has its own
.eslintrc.cjswithvue-eslint-parser,@typescript-eslint/strict, andplugin:vue/vue3-recommended. Runnpm run lintper package.
# Build all (main app + all web components)
# See Makefile for individual targets
# Build individual component
cd frontend/web-components/reaction-bar && npm install && npm run build -- --outDir ../../../static/wc/reaction-bar --emptyOutDir
# Lint individual component
cd frontend/web-components/reaction-bar && npm run lintAll visual customization flows through the facade dependency in src/app/deps/_index.yaml. This is the single source of truth for theming — it propagates to all host-rendered pages automatically.
Step A — Set colors in facade deps (src/app/deps/_index.yaml):
For a simple primary color change:
- name: css_variables
value: '{"--p-primary":"#6366f1"}'For full theme customization with all shades and dark mode, use custom_css:
- name: custom_css
value: ':root{--p-primary-50:#edf8f4;--p-primary-500:#15433D;...}@media(prefers-color-scheme:dark){:root{--p-primary:#1a9a7a;...}}'See navi-server's src/app/deps/_index.yaml for a complete real-world example.
This automatically recolors ALL host-rendered pages (view.page iframes).
Step B — Theme non-host pages manually.
Pages like static/login.html are outside the Web Host. Two approaches:
Recommended: Build with @wippy-fe/theme. If the page has any complexity, create it as a built app (even a tiny one) using @wippy-fe/theme — this gives you Tailwind, PrimeVue components, preflight resets, and theme variables out of the box, matching host-rendered pages exactly.
Simple pages: CDN imports. For trivial static pages (like a basic login form), import theme CSS directly:
- Import CSS via CDN URLs:
https://web-host.wippy.ai/webcomponents-1.0.12/@wippy-fe/assets/theme-config.css— theme variableshttps://web-host.wippy.ai/webcomponents-1.0.12/@wippy-fe/assets/preflight.css— Tailwind reset (normalizes form elements)https://web-host.wippy.ai/webcomponents-1.0.12/@wippy-fe/assets/tailwind.css— PrimeVue component styleshttps://web-host.wippy.ai/webcomponents-1.0.12/@wippy-fe/assets/iframe.css— scrollbar styling
- Use PrimeVue CSS classes on raw HTML elements (
p-inputtext,p-button,p-card, etc.) - Add the same CSS variable overrides as a
<style>block after the imports.
Fonts — Set via facade's custom_css parameter. You need both the font import AND a rule to apply it:
- name: custom_css
value: "@import url('https://fonts.googleapis.com/css2?family=YourFont:wght@400;500;600;700&display=swap'); body { font-family: 'YourFont', sans-serif; }"Note: The default facade config imports Poppins but does NOT apply it to the body — it just loads the font file. You must include
body { font-family: ... }(or set--v-font-family-headfor headings in the Web Host) for the font to actually take effect.
This injects into all host-rendered pages. For non-host pages (login, etc.), add the same @import and font-family rule manually.
Icons — Set via facade's icons parameter (Iconify format):
- name: icons
value: '{"logo":{"body":"<svg>...</svg>","width":24,"height":24}}'Icons are registered under the custom: namespace — use as custom:logo, custom:myicon, etc. Available in all host-rendered apps. For non-host pages, add icons manually (inline SVG, img tag, etc.).
If using host navigation (hide_nav_bar: "false"):
- Define the logo as a custom icon via the
iconsfacade parameter - Set
app_iconto reference it:app_icon: "custom:logo"
If rendering your own navigation (this template's default, hide_nav_bar: "true"):
- Replace
static/wippy-logo.svgwith your logo file - Update brand text ("Wippy App") in:
frontend/applications/main/src/app/app.vue— sidebar headerfrontend/applications/main/src/pages/home.vue— welcome pagestatic/login.html— login page titlefrontend/applications/main/app.html— browser tab title
Option A — Add a route to the existing app (when hide_nav_bar: "true", app manages its own sidebar):
- Create page component:
frontend/applications/main/src/pages/mypage.vue - Add route in
frontend/applications/main/src/router/index.ts - Add nav item in
app.vue'snavItemsarray - Build:
cd frontend/applications/main && npm run build -- --outDir ../../../static/app/main --emptyOutDir
Option B — Create a separate Vue app (when hide_nav_bar: "false", host provides navigation):
- Create a new app in
frontend/applications/myapp/(seefrontend/docs/app-guide.mdfor the full skeleton) - Register it in
src/app/views/_index.yamlas a newview.pageentry with its ownbase_pathandentry_point - Set
announced: trueso it appears in host navigation automatically - Build to
static/app/myapp/
All visual customization MUST go through the facade/host level, not individual apps.
The Wippy architecture serves multiple view.pages and web components under one host. If you add custom styles inside a single app, those styles will NOT apply to other pages or components — breaking visual consistency across the platform.
Rules:
- Use PrimeVue components for all form inputs, buttons, tables, dialogs, etc. — never create custom-styled inputs or buttons. PrimeVue components automatically pick up theme variables from the host, ensuring consistency everywhere.
- Customize appearance via facade parameters only —
css_variablesandcustom_cssinsrc/app/deps/_index.yaml. These propagate to ALL host-rendered pages and web components. - Use Tailwind utility classes with theme variables —
bg-primary,text-surface-700,border-surface-200etc. These reference CSS variables that the host controls. - Use semantic severity colors over raw Tailwind names — When a color conveys meaning (error, success, warning, info, help), use
danger-*,success-*,warn-*,info-*,help-*— neverred-*,green-*,orange-*,sky-*,purple-*. Raw color names are only for purely decorative use with no semantic meaning. In inline styles /v-html, usevar(--p-danger-500)etc. Semantics first, decorative later. - Explore existing variables in the browser first — before attempting any custom styling, open DevTools and inspect the available
--p-*CSS variables on:root. PrimeVue and Tailwind provide an extensive set of semantic variables for colors, spacing, borders, and typography. - Only use app-specific custom styles when you are FULLY aware they will stay unique to that app — for example, a custom data visualization or a layout that no other page shares. Even then, prefer CSS variables over hardcoded values.
Why this matters: If you style a button with custom CSS in one app, then add a second app or web component later, the button will look different. The facade's custom_css and css_variables are the ONLY mechanism that keeps all pages and components visually aligned.
Do NOT create web components unless you have a SOLID reason. Default to regular Vue components for everything.
Web components in Wippy exist for one purpose: cross-module reuse — shipping a self-contained UI widget that can be used in entirely different applications, potentially by different developers. They are standalone ES modules with Shadow DOM isolation, separate build outputs, and their own view.component registry entries.
When to use web components:
- You are building a widget intended to be shipped as a separate Wippy module that other projects can install and use (e.g., a Mermaid diagram renderer anyone can attach to their page)
- You are building a plugin system where renderers/widgets are loaded from external sources at runtime (e.g., a flow editor where node renderers can be shipped from other modules)
- You are developing/demonstrating web component examples (this repo's
frontend/web-components/exist solely as examples)
When NOT to use web components (use regular Vue components instead):
- The component is only used within your app — make it a
.vuefile insrc/components/ - The component is shared between pages in the same app — still a regular Vue component
- The component wraps a JS library for use in your app — regular Vue component
- You think "it might be reusable someday" — do NOT preemptively create a web component. Convert later if the need actually arises
- The component is part of a flow editor / diagram / complex UI but everything ships in the same app — regular Vue/JS components
The test: Ask yourself "will this component be loaded by a DIFFERENT application that I don't control?" If no, use a regular Vue component.
Why this matters: Web components have significant overhead — separate package, separate build, Shadow DOM isolation, separate
view.componentregistry entry, custom element registration, prop serialization via HTML attributes. None of this is needed for app-internal components. Using them unnecessarily adds complexity, slows development, and makes refactoring harder.
The Web Host provides a built-in left navigation sidebar. In this template it's hidden:
# In src/app/deps/_index.yaml, facade parameters:
- name: hide_nav_bar
value: "true"The main app (frontend/applications/main/) renders its own sidebar instead. Set hide_nav_bar to "false" to use the host's navigation — each view.page with announced: true appears as a nav item automatically.
Ensure all pages are usable with keyboard navigation and screen readers.
Form inputs:
- Every input MUST have an associated
<label>. For PrimeVueInputText, useidprop +<label for="...">. ForPasswordandSelect, useinputIdprop +<label for="...">. - Group related checkboxes/radios in a container with
role="group"andaria-label.
Buttons:
- Icon-only buttons MUST have
aria-labeldescribing the action. Include context where possible (e.g.,aria-label="Delete John Doe"not justaria-label="Delete"). - Decorative icons inside labeled buttons should have
aria-hidden="true".
Navigation:
- Use
<nav aria-label="...">for navigation regions. Use<aside aria-label="...">for sidebars. - Active nav item should have
aria-current="page".
Dialogs and errors:
- PrimeVue
Dialogwithmodalprop handles focus trapping automatically. - Error messages should use
role="alert"andaria-live="assertive"so screen readers announce them.
Tables:
- All columns need header text. Use
<span class="sr-only">...</span>for visually hidden headers (e.g., an Actions column).
Use this to verify the project follows guidelines after making changes.
- No web components created unless they are intended for cross-module reuse (loaded by a different application)
- App-internal shared UI is built as regular Vue components in
src/components/ - Existing web components in
frontend/web-components/are examples only — not a pattern to follow for app-specific features
- All form inputs, buttons, tables, dialogs use PrimeVue components — no custom-styled replacements
- No app-specific CSS overrides for colors, fonts, borders, or spacing that should be shared — all customization goes through facade
css_variables/custom_css - Tailwind classes reference theme variables (
bg-primary,text-surface-*,border-surface-*) — no hardcoded color values - Severity colors use semantic names (
danger-*,success-*,warn-*,info-*,help-*) — never rawred-*,green-*,orange-*,sky-*,purple-*for semantic uses - Before adding custom styles, existing
--p-*CSS variables were inspected in browser DevTools - Any app-specific styles are intentionally unique to that app and documented as such
- Colors set via facade
css_variablesorcustom_css— NOT hardcoded in Vue components - Non-host pages import theme CSS and apply the same variable overrides
- Login page colors match the main app theme
- Font set via facade
custom_css(withbody { font-family }rule) — non-host pages import the same font
- Custom icons defined via facade
iconsparameter - If using host nav:
app_iconreferences a valid icon (built-in orcustom:prefix) - If using custom nav: logo asset exists at referenced path
- Brand text updated in all locations if customized
-
hide_nav_barmatches navigation strategy - All
view.pageentries have correctbase_path,entry_point, and proxy config - Vue Router uses
createMemoryHistory()(not browser history) - Router calls
host.onRouteChanged()inafterEachand listens to@historyevents
- All form inputs have associated
<label>elements — usefor/idpairing forInputText,inputIdprop forPassword/Select - Icon-only buttons have
aria-labeldescribing the action (e.g.,aria-label="Delete John Doe") - Decorative icons have
aria-hidden="true"— icons inside labeled buttons, status indicators, etc. - Active navigation item has
aria-current="page" -
<nav>and<aside>elements havearia-label - Error messages use
role="alert"andaria-live="assertive"for screen reader announcements - PrimeVue
Dialogwithmodalprop handles focus trapping automatically — no custom implementation needed - Checkbox/radio groups wrapped in a container with
role="group"andaria-label - Table header cells cover all columns — use
<span class="sr-only">for visually hidden headers (e.g., Actions column)
- Frontend builds use
--outDirpointing tostatic/app/<name> - Vite config has
base: ""(empty string) - Host-provided libraries externalized in Vite config
-
@wippy-fe/themein dependencies, imported in app.ts and tailwind.config.ts
wippy run test users- wippy.ai - Full documentation
- wippy.ai/llms.txt - LLM-friendly docs
See frontend/docs/README.md for the full documentation index. Key guides:
component-guide.md— Web component development (WippyVueElement, props, events, Shadow DOM, PrimeVue)app-guide.md— Web app development (routing, Vite config, proxy API)best-practices.md— Vue 3, Tailwind CSS, accessibility, code qualityproxy-api.md— Wippy Proxy API referencehost-spec.md— Host contract specification