Version: 1.0.0
Last Updated: December 27, 2025
Author: SperaxOS Development Team
This guide covers the complete setup, development, and deployment of the SperaxOS plugin ecosystem, with a focus on portfolio integration in chat.
For developing external plugins deployed to the SperaxOS Plugin Marketplace (plugin.delivery), see the comprehensive guides in the plugins repository:
| Document | Description |
|---|---|
| Plugin Development Guide | Complete tutorial for building external plugins |
| Plugins README | Getting started with the plugin marketplace |
- Architecture Overview
- SperaxOS Plugin Ecosystem
- Portfolio Integration Methods
- Environment Setup
- Database Configuration
- Authentication Setup
- DeBank API Integration
- Builtin Plugin Development
- External Plugin Development
- Portal Components
- Embed Routes
- Artifacts
- Testing Plugins
- Deployment
- Troubleshooting
┌─────────────────────────────────────────────────────────────────────────┐
│ SPERAXOS │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ /chat │ │ /portfolio │ │ /embed │ │ /api │ │
│ │ (Main UI) │ │(Standalone)│ │ (Iframes) │ │ (Backend) │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ └───────────────┴───────────────┴───────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Zustand Stores │ │
│ │ - usePortfolioStore │ │
│ │ - useChatStore │ │
│ │ - useAgentStore │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Services Layer │ │
│ │ - portfolioService │ │
│ │ - DeBankClient │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ Database Layer │ │
│ │ - WalletModel │ │
│ │ - Drizzle ORM │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
User Message: "Show my portfolio"
│
▼
┌───────────────┐
│ AI Model │ ◄── System prompt includes plugin descriptions
└───────┬───────┘
│ Recognizes intent → Function Call
▼
┌───────────────┐
│ Tool Router │ ◄── Matches function to registered tool
└───────┬───────┘
│
├──► Builtin Tool? ──► Portal Component ──► Renders in chat
│
└──► External Plugin? ──► Gateway ──► Plugin Server ──► Response
| Repository | Purpose | NPM Package | Importance |
|---|---|---|---|
| plugin-sdk | SDK for building plugins | @nirholas/plugin-sdk |
🔴 Critical |
| chat-plugins-gateway | Proxy between SperaxOS and plugins | @nirholas/chat-plugins-gateway |
🔴 Critical |
| speraxos-plugins | Plugin marketplace index | - | 🟡 Medium |
| openai-plugins | ChatGPT plugin compatibility | - | 🟢 Low |
| Repository | Type | Has Backend | Has Frontend | Deployment |
|---|---|---|---|---|
| chat-plugin-template | Template | ✅ | ✅ | Vercel |
| chat-plugin-web-crawler | Default | ✅ | ❌ | Vercel |
| chat-plugin-search-engine | Default | ✅ | ❌ | Vercel |
| chat-plugin-clock-time | Standalone | ❌ | ✅ | Vercel Static |
| chat-plugin-realtime-weather | Default | ✅ | ✅ | Vercel |
| chat-plugin-bilibili | Default | ✅ | ❌ | Vercel |
| chat-plugin-steam | Default | ✅ | ❌ | Vercel |
| chat-plugin-open-interpreter | Standalone | ✅ | ✅ | 🔴 Local Only |
# For plugin development
pnpm add @nirholas/plugin-sdk
# For gateway setup
pnpm add @nirholas/chat-plugins-gatewayimport { speraxOS } from '@nirholas/plugin-sdk/client';
// Get initialization data
const payload = await speraxOS.getPluginPayload();
// { name, arguments, settings, state }
// Update message content
speraxOS.setPluginMessage('New content');
// Update plugin state
speraxOS.setPluginState('key', value);
// Trigger AI response (standalone plugins)
speraxOS.triggerAIMessage(messageId);
// Create assistant message (standalone plugins)
speraxOS.createAssistantMessage('AI will process this');| Method | Best For | Interactivity | Complexity | Auth Context |
|---|---|---|---|---|
| Portal | Native components | High | Medium | ✅ Shared |
| Iframe Embed | Full dashboards | Very High | Low | |
| Artifacts | Custom visualizations | Medium | Low | ❌ Sandboxed |
| Markdown | Quick text responses | None | Very Low | N/A |
| Function + AI | Data + interpretation | None | Low | ✅ Backend |
| Standalone | Complex workflows | Full Control | High | Plugin-managed |
User asks about portfolio
├── Quick answer needed?
│ └── YES → Markdown response or Function + AI Summary
│
├── Visual display needed?
│ ├── Custom one-off chart → Artifact
│ ├── Standard portfolio view → Portal Component
│ └── Full dashboard → Iframe Embed
│
└── Complex interaction needed?
└── Standalone Plugin
| Method | Status | Location |
|---|---|---|
| Portal | ✅ Built | src/features/Portal/PortfolioAnalytics/ |
| Iframe Embed | ✅ Built | src/app/[variants]/(portfolio)/embed/* |
| Artifacts | ✅ Ready | AI can generate anytime |
| Markdown | Plugin exists, needs formatter | |
| Function + AI | Needs data fetching | |
| Standalone | ❌ Not built | Future work |
Create .env.local in project root:
# ============================================
# DATABASE
# ============================================
DATABASE_URL=postgresql://user:password@host/database?sslmode=require
# ============================================
# AUTHENTICATION
# ============================================
# Option A: Clerk (Recommended for production)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
CLERK_WEBHOOK_SECRET=whsec_xxx
# Option B: NextAuth
NEXT_AUTH_SECRET=your-secret-key-min-32-chars
KEY_VAULTS_SECRET=base64-encoded-32-byte-key
# ============================================
# APP CONFIGURATION
# ============================================
NEXT_PUBLIC_SERVICE_MODE=server
APP_URL=http://localhost:3010
# ============================================
# DEBANK API (Optional - Free tier works without key)
# ============================================
# DEBANK_API_KEY=your-api-key # Only if using Pro tier
# ============================================
# AI PROVIDERS (Configure at least one)
# ============================================
OPENAI_API_KEY=sk-xxx
# ANTHROPIC_API_KEY=sk-xxx
# GOOGLE_API_KEY=xxx# Generate KEY_VAULTS_SECRET (32 bytes, base64 encoded)
openssl rand -base64 32
# Generate NEXT_AUTH_SECRET
openssl rand -hex 16| Database | Use Case | Setup |
|---|---|---|
| Neon PostgreSQL | Production | Cloud-hosted |
| Local PostgreSQL | Development | Docker |
| PGLite | Browser/Development | Built-in |
-
Create Neon Account: https://neon.tech
-
Create Database:
- Create new project
- Copy connection string
- Enable pgvector extension:
CREATE EXTENSION IF NOT EXISTS vector;
-
Configure Environment:
DATABASE_URL=postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require -
Push Schema:
bunx drizzle-kit push
The schema includes these portfolio-specific tables:
// src/database/schemas/portfolio.ts
// User wallet addresses
user_wallets: {
id: string;
userId: string;
address: string; // 0x...
chain: string; // ethereum, polygon, etc.
label: string; // "Main Wallet"
isPrimary: boolean;
createdAt: timestamp;
}
// Token watchlist
portfolio_watchlist: {
id: string;
userId: string;
tokenSymbol: string;
tokenAddress: string;
chain: string;
addedAt: timestamp;
}
// Historical snapshots
portfolio_snapshots: {
id: string;
userId: string;
totalValueUsd: number;
snapshotAt: timestamp;
data: jsonb; // Full snapshot data
}
// Notification preferences
portfolio_notification_prefs: {
id: string;
userId: string;
priceAlerts: boolean;
dailyDigest: boolean;
weeklyReport: boolean;
settings: jsonb;
}# Check if tables were created
bunx drizzle-kit studio
# Or query directly
psql $DATABASE_URL -c "\dt"-
Create Clerk Account: https://clerk.com
-
Create Application:
- Choose sign-in methods (email, Google, GitHub, etc.)
- Copy publishable key and secret key
-
Configure Environment:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx CLERK_SECRET_KEY=sk_test_xxx
-
Webhook Setup (for user sync):
- In Clerk Dashboard → Webhooks
- Add endpoint:
https://your-domain.com/api/webhooks/clerk - Copy webhook secret:
CLERK_WEBHOOK_SECRET=whsec_xxx
-
Verify Integration:
// Auth is automatically detected import { getUserAuth } from '@sperax/utils/server'; const { userId } = await getUserAuth(); // Returns Clerk user ID when enabled
-
Generate Secrets:
# Auth secret openssl rand -hex 16 # Key vault secret openssl rand -base64 32
-
Configure Environment:
NEXT_AUTH_SECRET=your-hex-secret KEY_VAULTS_SECRET=your-base64-secret
-
Configure OAuth Providers (optional):
# GitHub AUTH_GITHUB_ID=xxx AUTH_GITHUB_SECRET=xxx # Google AUTH_GOOGLE_ID=xxx AUTH_GOOGLE_SECRET=xxx
// API Route (Backend)
import { getUserAuth } from '@sperax/utils/server';
export async function GET(request: Request) {
const { userId } = await getUserAuth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const walletModel = new WalletModel(serverDB, userId);
const wallets = await walletModel.getWallets();
return NextResponse.json({ data: wallets });
}| Tier | Cost | Rate Limit | API Key Required |
|---|---|---|---|
| Free | $0 | 1000 calls/day | ❌ No |
| Pro | $200+/month | Higher | ✅ Yes |
The DeBank client auto-detects which tier to use:
// src/server/services/debank/client.ts
constructor(options: DeBankClientOptions = {}) {
this.apiKey = options.apiKey || process.env.DEBANK_API_KEY;
// Auto-fallback to free tier if no API key
this.baseUrl = this.apiKey
? 'https://pro-openapi.debank.com' // Pro
: 'https://openapi.debank.com'; // Free
}// Token balances
debankClient.getAllTokens(address); // All tokens across chains
debankClient.getTokensOnChain(address, chainId); // Chain-specific
// DeFi positions
debankClient.getAllProtocols(address); // All protocol positions
// Balance
debankClient.getTotalBalance(address); // Total USD value
// Transaction history
debankClient.getHistoryList(address); // Recent transactionsBuilt-in in-memory caching reduces API calls:
const CACHE_TTL = {
BALANCE: 60, // 1 minute
TOKENS: 300, // 5 minutes
PROTOCOLS: 300, // 5 minutes
HISTORY: 600, // 10 minutes
CHAINS: 86_400, // 24 hours
};Builtin plugins are part of SperaxOS and don't need external deployment.
src/tools/
├── index.ts # Registers all builtin tools
├── portals.ts # Maps tools to portal components
├── sperax-portfolio/
│ ├── index.ts # Main export
│ └── manifest.ts # Plugin manifest
├── artifacts/
├── dalle/
├── web-browsing/
└── code-interpreter/
// src/tools/my-plugin/manifest.ts
import { BuiltinToolManifest } from '@sperax/types';
export const myPluginManifest: BuiltinToolManifest = {
identifier: 'my-plugin',
api: [
{
name: 'myFunction',
description: 'Description for AI to understand when to use',
parameters: {
type: 'object',
properties: {
param1: {
type: 'string',
description: 'What this parameter does',
},
param2: {
type: 'number',
default: 10,
description: 'Optional parameter with default',
},
},
required: ['param1'],
},
},
],
meta: {
avatar: '🔧',
title: 'My Plugin',
description: 'What my plugin does',
tags: ['utility', 'example'],
},
systemRole: `You are an assistant with access to My Plugin.
Use myFunction when the user wants to...`,
type: 'builtin',
};// src/tools/my-plugin/index.ts
export { myPluginManifest } from './manifest';
export { myPluginManifest as default } from './manifest';// src/tools/index.ts
import { myPluginManifest } from './my-plugin';
export const builtinTools: SperaxOSBuiltinTool[] = [
// ... existing tools
{
identifier: myPluginManifest.identifier,
manifest: myPluginManifest,
type: 'builtin',
},
];// src/features/Portal/MyPlugin/index.tsx
import { memo, Suspense } from 'react';
import type { BuiltinPortalProps } from '@sperax/types';
const MyPluginBody = memo<{ payload?: Record<string, any> }>(({ payload }) => {
const { param1, param2 } = payload || {};
return (
<div>
<h3>My Plugin Result</h3>
<p>Param1: {param1}</p>
<p>Param2: {param2}</p>
</div>
);
});
export const MyPluginPortal = memo<BuiltinPortalProps>(({ arguments: args }) => {
return <MyPluginBody payload={args} />;
});// src/tools/portals.ts
import { MyPluginPortal } from '@/features/Portal/MyPlugin';
export const BuiltinToolsPortals: Record<string, BuiltinPortal> = {
// ... existing portals
'my-plugin___myFunction': MyPluginPortal as BuiltinPortal,
};External plugins run on separate servers and communicate via Gateway.
# Clone template
git clone https://github.com/nirholas/chat-plugin-template my-plugin
cd my-plugin
# Install dependencies
pnpm install
# Start development
pnpm dev// public/manifest.json
{
"$schema": "https://plugin.delivery/schema/manifest-v1.json",
"identifier": "my-external-plugin",
"version": "1.0.0",
"api": [
{
"name": "getData",
"description": "Fetches data from external API",
"url": "https://my-plugin.vercel.app/api/getData",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
],
"meta": {
"title": "My External Plugin",
"description": "Does something useful",
"avatar": "🔌",
"tags": ["external", "api"]
},
"gateway": "https://my-plugin.vercel.app/api/gateway"
}// pages/api/getData.ts (or app/api/getData/route.ts)
export default async function handler(req, res) {
const { query } = req.body;
// Process request
const result = await fetchExternalData(query);
// Return response
res.json({ data: result });
}// pages/api/gateway.ts
import { createSperaxOSPluginGateway } from '@nirholas/chat-plugins-gateway';
export default createSperaxOSPluginGateway();// In manifest.json
{
"settings": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"title": "API Key",
"description": "Your API key for the external service"
}
},
"required": ["apiKey"]
}
}Access settings in API:
import { getPluginSettingsFromRequest } from '@nirholas/chat-plugins-gateway';
export default async function handler(req, res) {
const settings = getPluginSettingsFromRequest(req);
const apiKey = settings.apiKey;
// Use API key...
}Portals render plugin output directly in the chat UI.
// src/features/Portal/PortfolioAnalytics/index.tsx
import { memo, Suspense, lazy } from 'react';
import { createStyles } from 'antd-style';
import { Spin } from 'antd';
import type { BuiltinPortalProps } from '@sperax/types';
const useStyles = createStyles(({ css, token }) => ({
container: css`
width: 100%;
min-height: 400px;
background: ${token.colorBgContainer};
border-radius: ${token.borderRadiusLG}px;
`,
loading: css`
display: flex;
align-items: center;
justify-content: center;
height: 400px;
`,
}));
// Lazy load components
const Dashboard = lazy(() => import('@/components/portfolio/dashboard-content'));
const PortalBody = memo<{ payload?: Record<string, any> }>(({ payload }) => {
const { styles } = useStyles();
const { view = 'dashboard' } = payload || {};
return (
<div className={styles.container}>
<Suspense fallback={<Spin size="large" />}>
{view === 'dashboard' && <Dashboard />}
{/* Add more views */}
</Suspense>
</div>
);
});
// Export for portal registration
export const PortfolioAnalyticsPortal = memo<BuiltinPortalProps>(
({ arguments: args }) => <PortalBody payload={args} />
);// src/tools/portals.ts
import { PortfolioAnalyticsPortal } from '@/features/Portal/PortfolioAnalytics';
export const BuiltinToolsPortals: Record<string, BuiltinPortal> = {
// Format: 'plugin-identifier___functionName'
'sperax-portfolio___showPortfolio': PortfolioAnalyticsPortal,
'sperax-portfolio___getPortfolioSummary': PortfolioAnalyticsPortal,
'sperax-portfolio___searchAssets': PortfolioAnalyticsPortal,
'sperax-portfolio___analyzePortfolio': PortfolioAnalyticsPortal,
};Embed routes provide full portfolio views for iframe embedding.
| Route | Component | Description |
|---|---|---|
/embed/dashboard |
DashboardContent | Full portfolio dashboard |
/embed/wallets |
WalletsInterface | Wallet management |
/embed/trading |
TradingInterface | Trading UI |
/embed/assets |
MyAssets | Asset list |
/embed/analytics |
MyAnalytics | Charts and analytics |
/embed/defi-protocols |
DefiProtocolsInterface | DeFi positions |
/embed/yield-farming |
YieldFarmingInterface | Yield opportunities |
/embed/staking-pools |
StakingPoolsInterface | Staking pools |
/embed/ai-bot |
AiBotDashboard | AI trading bot |
/embed/dca-bot |
DcaBotDashboard | DCA automation |
/embed/signal-bot |
SignalBotDashboard | Signal-based trading |
/embed/settings |
SettingsInterface | Settings |
/embed/help-center |
HelpCenterInterface | Help & FAQ |
// src/app/[variants]/(portfolio)/embed/dashboard/page.tsx
'use client';
import { useEffect } from 'react';
import { DashboardContent } from '@/components/portfolio/dashboard-content';
import { useEmbedMode } from '@/libs/portfolio/embed-utils';
export default function DashboardEmbedPage() {
const embed = useEmbedMode();
useEffect(() => {
if (embed.isEmbed) {
embed.notifyReady(embed);
}
}, [embed]);
return (
<div style={{ height: '100%', overflow: 'auto' }}>
<DashboardContent />
</div>
);
}// src/libs/portfolio/embed-utils.ts
export enum MessageType {
PORTFOLIO_READY = 'PORTFOLIO_READY',
PORTFOLIO_DATA_LOADED = 'PORTFOLIO_DATA_LOADED',
SET_ACCOUNT = 'SET_ACCOUNT',
SET_THEME = 'SET_THEME',
REFRESH_PORTFOLIO = 'REFRESH_PORTFOLIO',
}
// Send to parent window
export function sendToParent(type: MessageType, data?: any) {
if (typeof window === 'undefined' || !isIframe()) return;
window.parent.postMessage({ type, data }, '*');
}
// Listen from parent
export function listenToParent(callback: (message) => void) {
window.addEventListener('message', (event) => {
if (event.data?.type) callback(event.data);
});
}<speraxArtifact identifier="portfolio-embed" type="text/html" title="Portfolio">
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; }
iframe { width: 100%; height: 600px; border: none; }
</style>
</head>
<body>
<iframe src="/embed/dashboard" title="Portfolio"></iframe>
</body>
</html>
</speraxArtifact>Artifacts are AI-generated React components rendered in chat.
| Library | Version | Use For |
|---|---|---|
react |
19.x | Components, hooks |
antd |
5.x | UI components |
lucide-react |
0.263.1 | Icons |
recharts |
2.x | Charts |
<speraxThinking>
Analyzing user request...
</speraxThinking>
<speraxArtifact
identifier="unique-id"
type="application/sperax.artifacts.react"
title="Display Title"
>
import React from 'react';
import { Card, Statistic, Row, Col } from 'antd';
import { TrendingUp, Wallet } from 'lucide-react';
export default function App() {
// Component code
return (
<Card title="Portfolio Summary">
<Row gutter={16}>
<Col span={12}>
<Statistic title="Total Value" value={127453.89} prefix="$" />
</Col>
<Col span={12}>
<Statistic title="24h Change" value={2.28} suffix="%" />
</Col>
</Row>
</Card>
);
}
</speraxArtifact>| Scenario | Recommended |
|---|---|
| Custom one-off visualization | ✅ Yes |
| Displaying static data | ✅ Yes |
| Interactive forms | |
| Accessing app stores | ❌ No |
| Persistent state | ❌ No |
# Start SperaxOS dev server
bun run dev
# For external plugins, start plugin server
cd my-plugin && pnpm dev- Start dev server
- Go to http://localhost:3010/chat
- Enable plugin in agent settings
- Test by asking related questions
- Start plugin server locally
- In SperaxOS → Settings → Plugins → Custom Plugins
- Add manifest URL:
http://localhost:3400/manifest.json - Enable and test
# Enable debug logging
DEBUG=sperax:* bun run dev# Check for TypeScript errors
bun run type-check
# Run tests
bunx vitest run --silent='passed-only' 'src/tools'Platform: Vercel
Configuration:
// vercel.json
{
"buildCommand": "bun run build",
"framework": "nextjs"
}Environment Variables to Set:
DATABASE_URLNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEYCLERK_WEBHOOK_SECRETOPENAI_API_KEYNEXT_PUBLIC_SERVICE_MODE=server
Deploy:
# Push to deploy branch
git push origin feature/xxx:deployPlatform: Vercel (recommended)
# Deploy
vercel --prod
# Or connect GitHub repo for auto-deployRequired Setup:
- Add environment variables in Vercel dashboard
- Configure custom domain (optional)
- Update manifest.json with production URLs
If using separate gateway:
# Deploy gateway
vercel --prod
# Update plugins to use gateway URLCause: User not authenticated
Solution:
- Ensure Clerk or NextAuth is configured
- Check
NEXT_PUBLIC_ENABLE_CLERK_AUTHis set - Verify user is logged in
Cause: User hasn't connected wallet in /portfolio
Solution:
- Go to /portfolio
- Connect MetaMask/wallet
- Wallet saves to
user_walletstable - Try chat command again
Cause: createStyles returns class names, not style objects
Wrong:
<div style={styles.container}> // ❌ ErrorCorrect:
<div className={styles.container}> // ✅ CorrectCause: pgvector extension not enabled
Solution:
CREATE EXTENSION IF NOT EXISTS vector;Cause: Not registered in builtinTools or portals
Solution:
- Add to
src/tools/index.ts - Add portal to
src/tools/portals.ts - Restart dev server
Cause: Route not defined or layout missing
Solution:
- Check route exists in
src/app/[variants]/(portfolio)/embed/ - Verify layout.tsx exists in parent folder
# Check environment
env | grep -E "(DATABASE|CLERK|NEXT)"
# Check database tables
bunx drizzle-kit studio
# Check TypeScript
bun run type-check 2>&1 | head -50
# Check build
bun run build 2>&1 | tail -100
# Clear cache
rm -rf .next node_modules/.cachesrc/
├── tools/
│ ├── index.ts # Builtin tools registry
│ ├── portals.ts # Portal component mapping
│ └── sperax-portfolio/
│ ├── index.ts
│ └── manifest.ts
│
├── features/
│ └── Portal/
│ └── PortfolioAnalytics/
│ └── index.tsx # Portfolio portal component
│
├── components/
│ └── portfolio/ # All portfolio components
│ ├── dashboard-content/
│ ├── my-assets/
│ ├── my-analytics/
│ └── ...
│
├── app/
│ └── [variants]/
│ └── (portfolio)/
│ └── embed/ # 26 embed routes
│ ├── dashboard/
│ ├── wallets/
│ └── ...
│
├── libs/
│ └── portfolio/
│ └── embed-utils.ts # Iframe communication
│
├── server/
│ └── services/
│ └── debank/
│ ├── client.ts # DeBank API client
│ └── types.ts
│
├── services/
│ └── portfolio.ts # Frontend portfolio service
│
├── store/
│ └── portfolio/ # Zustand portfolio store
│
└── database/
└── schemas/
└── portfolio.ts # DB schema for wallets, etc.
# .env.local.example
# Database
DATABASE_URL=
# Auth (choose one)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
# OR
NEXT_AUTH_SECRET=
KEY_VAULTS_SECRET=
# App
NEXT_PUBLIC_SERVICE_MODE=server
APP_URL=http://localhost:3010
# AI (at least one)
OPENAI_API_KEY=
# Optional
DEBANK_API_KEY=| Type | Backend | Frontend | AI Summarizes |
|---|---|---|---|
| default | ✅ Required | Optional | ✅ Yes |
| markdown | ✅ Required | ❌ No | ❌ No |
| standalone | Optional | ✅ Required | Controlled |
User message → AI → Function Call → Tool Router
→ Builtin? → Portal → Render
→ External? → Gateway → Plugin Server → Response → AI → Display
- Environment variables configured
- Database migrated
- Auth provider configured
- At least one AI provider configured
- Build passes (
bun run build) - Type check passes (
bun run type-check)
End of Document