Technology Stack
| Layer | Technology | Version | Justification |
|---|---|---|---|
| Web Framework | Hono | 4.x | Fastest framework (2-3x Express, 5x NestJS), tiny bundle, zero magic |
| Runtime | Node.js | 20+ LTS | ESM-native, stable |
| Database | SQLite (better-sqlite3) | latest | Embedded, zero setup, WAL mode for concurrent reads |
| ORM | Drizzle ORM | 0.30+ | TypeScript-first, zero-runtime overhead |
| Real-time | Socket.IO | 4.x | Battle-tested WebSocket with rooms and reconnect logic |
| Task Queue | Custom In-Memory Priority Queue | - | For single-process, Map<priority, Queue> sufficient |
| Frontend | Vite | 5.x | Lightning-fast HMR (<50ms), 3-5x smaller than Next.js |
| UI Framework | React | 18.x | Declarative, concurrent, stable |
| State | Zustand | 4.x | Minimal, hooks-first, no boilerplate |
| Server State | TanStack Query | 5.x | Intelligent caching and background refetch |
| Styling | Vanilla CSS + CSS Modules | - | Custom glassmorphism design system |
| Offline | IndexedDB (idb) | 8.x | Offline command queue and cursor persistence |
| Monorepo | pnpm workspaces | latest | Package manager + workspace orchestration |
| Testing | Vitest | latest | Faster than Jest, native ESM support |
Purpose and Philosophy
GATOR Lite (codename APEX) is the lean, ultra-fast successor to GATOR v1. It is a private, AI-powered orchestration system that provides chat-first, intent-based remote control over your Antigravity IDE workspace from any device — primarily Android mobile.
The core philosophy centers on "Phone as Control Surface Only" — the mobile client has no direct host access. All execution routes through the core → LSP-Bridge chain. The phone emits intent, never commands.
Core Design Principles
- UI is a Lens, Not Source of Truth: The APEX Core owns all state. Clients read snapshots and emit intent. The UI never computes business logic locally.
- Every Action is a Named Whitelisted Task:
task-registry.jsonis the security contract. Unknown actions are rejected before execution.
- LSP-Bridge, Never UI Scraping: All IDE communication goes through PORTA's verified Connect RPC protocol. No CDP, no DOM automation, no UI pixel-scraping. 100% ToS-compliant.
- Network Exposure Guard: The Core binds only to loopback, private LAN, or Tailscale CGNAT. Wildcard binds (
0.0.0.0) throw at startup.
- Offline First: IndexedDB queue on client. SQLite WAL persists events. Tasks survive Core restarts.
- Zero-Config Startup: No Docker. No database setup. Run
pnpm devand the system is live with a self-contained SQLite file.
- Lean by Discipline: Before adding any dependency: does this justify its weight? If a built-in Node.js API can do it, no library is added.
Architecture Deep Dive
System Topology
The entire system runs as one lightweight Hono process with embedded SQLite:
┌─────────────────────────────────────────────────────────────────────┐
│ APEX CORE (Hono — Single Process) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth │ │ Chat Router │ │ WS Server │ │
│ │ (JWT/OTP) │ │ + Intent │ │ (Socket.IO) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────────▼────────────────────┐ │
│ │ Orchestrator (Task Lifecycle) │ │
│ │ validate → deduplicate → enqueue → track → notify │ │
│ └───────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┐ ┌─┴───────────────────────┐ │
│ │ SQLite (WAL) │ │ In-Memory Queue │ │
│ │ (Drizzle ORM) │ │ low | med | high │ │
│ └────────────────┘ └───────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ LSP-BRIDGE (PORTA — embedded in Core) │ │
│ │ ls-discovery → RPC Client → Delta-Polling → Step Recovery│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Execution Sandbox (child_process.spawn) │ │
│ │ Whitelisted commands only | restricted PATH │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Database Schema (SQLite + Drizzle)
// packages/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
// Devices
export const devices = sqliteTable('devices', {
id: text('id').primaryKey(),
name: text('name').notNull(),
platform: text('platform').notNull(),
jwtFingerprint: text('jwt_fingerprint').notNull().unique(),
pairedAt: integer('paired_at', { mode: 'timestamp' }).notNull(),
lastSeen: integer('last_seen', { mode: 'timestamp' }).notNull(),
role: text('role').notNull().default('operator'),
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
});
// Tasks
export const tasks = sqliteTable('tasks', {
id: text('id').primaryKey(),
deviceId: text('device_id').notNull(),
action: text('action').notNull(),
target: text('target'),
status: text('status').notNull(),
riskLevel: text('risk_level'),
resultSummary: text('result_summary'),
exitCode: integer('exit_code'),
durationMs: integer('duration_ms'),
submittedAt: integer('submitted_at', { mode: 'timestamp' }).notNull(),
startedAt: integer('started_at', { mode: 'timestamp' }),
completedAt: integer('completed_at', { mode: 'timestamp' }),
});
// Conversations
export const conversations = sqliteTable('conversations', {
cascadeId: text('cascade_id').primaryKey(),
workspaceId: text('workspace_id').notNull(),
threadId: text('thread_id').unique(),
title: text('title'),
stepCount: integer('step_count').notNull().default(0),
status: text('status').notNull().default('idle'),
lastSyncedAt: integer('last_synced_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
// Settings
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull(),
});
// JWT Blocklist
export const jwtBlocklist = sqliteTable('jwt_blocklist', {
fingerprint: text('fingerprint').primaryKey(),
revokedAt: integer('revoked_at', { mode: 'timestamp' }).notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
});Intent Parser
// Rule-based intent parsing
interface ParsedIntent {
action: string;
target: string;
confidence: number;
riskLevel: 'low' | 'medium' | 'high';
}
// Example mappings
const intentRules = [
{ pattern: /run\s+tests?\s+on\s+(\w+)/i, action: 'run_tests', target: '$1' },
{ pattern: /build\s+(\w+)/i, action: 'build_project', target: '$1' },
{ pattern: /deploy/i, action: 'deploy', target: 'production' },
];
function parseIntent(message: string): ParsedIntent {
for (const rule of intentRules) {
const match = message.match(rule.pattern);
if (match) {
const target = rule.target.replace('$1', match[1] || '');
return {
action: rule.action,
target,
confidence: 0.97,
riskLevel: getRiskLevel(rule.action),
};
}
}
return { action: 'unknown', target: '', confidence: 0 };
}Task Orchestrator
// packages/orchestrator/task-orchestrator.ts
async function processIntent(deviceId: string, message: string) {
// 1. Parse intent
const intent = parseIntent(message);
// 2. Validate against task registry
const taskDef = taskRegistry[intent.action];
if (!taskDef) {
throw new Error('Action not permitted');
}
// 3. Generate deterministic task ID
const taskId = sha256(`${deviceId}${intent.action}${intent.target}${Date.now()}`);
// 4. Check for duplicates (dedup)
const existing = await getTaskFromQueue(taskId);
if (existing) return { taskId, status: 'duplicate' };
// 5. Enqueue with priority
const priority = taskDef.riskLevel === 'high' ? 'high' :
taskDef.riskLevel === 'medium' ? 'medium' : 'low';
await enqueueTask({ taskId, ...intent, priority, deviceId });
// 6. Persist to SQLite
await db.insert(tasks).values({
id: taskId,
deviceId,
action: intent.action,
target: intent.target,
status: 'queued',
riskLevel: taskDef.riskLevel,
submittedAt: new Date(),
});
return { taskId, status: 'queued' };
}Execution Sandbox
// packages/task-runner/sandbox.ts
const ALLOWED_BINARIES = ['node', 'npm', 'npx', 'pnpm', 'git', 'docker', 'pm2'];
const RESTRICTED_PATH = '/usr/local/bin:/usr/bin:/bin';
async function executeTask(task: QueuedTask) {
const command = taskRegistry[task.action].command;
const args = resolveArgs(task.action, task.target);
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: resolveWorkspacePath(task.target),
env: {
PATH: RESTRICTED_PATH,
HOME: process.env.HOME,
NODE_ENV: 'production',
},
uid: process.getuid(), // non-root assertion
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.on('data', (chunk) => {
// Stream to WebSocket
ws.to(task.id).emit('task:log', { chunk, seq: seq++ });
});
child.stderr.on('data', (chunk) => {
ws.to(task.id).emit('task:log', { chunk: `[ERR] ${chunk}`, seq: seq++ });
});
child.on('close', (code) => {
resolve({ exitCode: code, durationMs: Date.now() - start });
});
});
}LSP-Bridge Integration
The system integrates with the Antigravity Language Server via Connect RPC:
// packages/lsp-bridge/rpc.ts
interface LSInstance {
pid: number;
httpsPort: number;
csrfToken: string;
workspaceId: string;
}
// Connect RPC call
async function callLSMethod(method: string, payload: unknown, instance: LSInstance) {
const response = await fetch(
`https://127.0.0.1:${instance.httpsPort}/exa.language_server_pb.LanguageServerService/${method}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-codeium-csrf-token': instance.csrfToken,
},
body: JSON.stringify(payload),
rejectUnauthorized: false,
}
);
return response.json();
}
// Start AI conversation
async function startCascade(workspaceId: string, message: string) {
const instance = await discoverLS(workspaceId);
return callLSMethod('StartCascade', {
workspaceId,
messages: [{ role: 'user', content: message }],
metadata: { ideName: 'gator-lite', ideVersion: '1.0.0' },
}, instance);
}
// Send message to running conversation
async function sendMessage(cascadeId: string, message: string) {
return callLSMethod('SendUserCascadeMessage', {
cascadeId,
items: [{ text: message }],
}, instance);
}Delta Polling State Machine
// packages/lsp-bridge/delta-poller.ts
const ACTIVE_INTERVAL = 150; // ms (reduced from 50ms in v1)
const IDLE_INTERVAL = 20000; // ms (increased from 15000ms)
const EMPTY_THRESHOLD = 5; // consecutive empty polls before IDLE
const OVERLAP_COUNT = 5; // trailing steps re-fetched per poll
let state = 'IDLE';
let consecutiveEmpty = 0;
async function poll() {
const steps = await fetchSteps(cascadeId, offset);
if (steps.length > 0) {
consecutiveEmpty = 0;
socket.to(cascadeId).emit('cascade:steps', { offset, steps });
offset += steps.length;
} else {
consecutiveEmpty++;
if (consecutiveEmpty >= EMPTY_THRESHOLD) {
state = 'IDLE';
}
}
}
setInterval(() => {
if (state === 'ACTIVE') poll();
}, state === 'ACTIVE' ? ACTIVE_INTERVAL : IDLE_INTERVAL);
// Activate on user interaction
function activate() {
state = 'ACTIVE';
consecutiveEmpty = 0;
poll();
}Security Architecture
OTP Pairing Flow
// 1. Desktop opens /api/auth/otp (desktop-only)
const otp = generateOTP(); // 6-digit
await db.insert(otps).values({ code: otp, expiresAt: now + 5min });
// 2. Phone submits POST /api/auth/pair
const device = await db.select().from(devices).where(eq(otps.code, otp));
if (!device) throw new Error('Invalid OTP');
// 3. Core creates device and signs JWT
const token = await signJWT({ sub: device.id, fingerprint: sha256(token) });
return { token, expiresAt: now + 30d };Network Exposure Guard
// Block wildcards at startup
const allowed = ['127.0.0.1', 'localhost', '::1', '10.x.x.x', '172.16-31.x.x', '192.168.x.x'];
if (process.env.PORTA_TAILSCALE) allowed.push('100.64.x.x');
if (!allowed.includes(CORE_HOST)) {
throw new Error('Public internet exposure unsupported');
}API Endpoints
Auth & Devices
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/pair | None | Submit OTP, receive JWT |
| POST | /api/auth/refresh | JWT | Refresh JWT |
| GET | /api/auth/otp | Desktop | Generate pairing OTP |
| GET | /api/devices | JWT | List paired devices |
| DELETE | /api/devices/:id | JWT | Revoke device |
Chat & Tasks
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/chat | JWT | Submit intent message |
| GET | /api/tasks | JWT | List recent tasks |
| GET | /api/tasks/:id/logs | JWT | Get full log output |
| POST | /api/tasks/:id/confirm | JWT | Confirm high-risk task |
Conversations
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/conversations | JWT | List all cascades |
| POST | /api/conversations/:id/messages | JWT | Send message |
| POST | /api/conversations/:id/stop | JWT | Cancel cascade |
WebSocket Protocol
Server → Client events:
socket.on('task:queued', { taskId, action, target, riskLevel })
socket.on('task:running', { taskId })
socket.on('task:log', { taskId, chunk, seq })
socket.on('task:complete', { taskId, status, exitCode, durationMs })
socket.on('cascade:steps', { cascadeId, offset, steps })
socket.on('cascade:status', { cascadeId, running })
socket.on('system:alert', { level, message })Frontend Architecture
Design System - "Glass & Glow"
:root {
--color-bg-base: hsl(220, 20%, 6%);
--color-bg-surface: hsl(220, 18%, 10%);
--color-bg-glass: hsla(220, 18%, 15%, 0.6);
--color-accent: hsl(164, 80%, 50%);
--glass-blur: 16px;
--glass-border: 1px solid hsla(220, 18%, 50%, 0.15);
--font-ui: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}PWA Configuration
{
"name": "GATOR Lite",
"short_name": "GATOR",
"display": "standalone",
"start_url": "/",
"background_color": "#0d0f17",
"theme_color": "#0d0f17"
}Key Features
- Chat-First Intent Control: Type natural language on phone, see real-time steps stream back
- Task Execution: Run whitelisted shell tasks from anywhere
- Live Log Streaming: Real-time stdout/stderr via WebSocket
- Conversation Management: Browse, resume, revert, delete AI conversations
- Session Audit: Every action recorded, every device tracked
- Offline Queue: Commands survive connection drops and replay in order
- Delta Polling: 150ms intervals when active, 20s when idle
- Step Recovery: 3-tier recovery for corrupted steps
Performance Comparison
| Metric | GATOR v1 | GATOR Lite |
|---|---|---|
| RAM Usage | ~720MB | <150MB |
| Startup Time | ~30s | <1.5s |
| Cold Start | ~500ms | ~50ms |
| Bundle Size | ~250KB | ~50KB |
| Docker Containers | 5 | 0 |
Build and Run
# Install dependencies
pnpm install
# Create env file
cp .env.example .env
# Edit: JWT_SECRET, CORE_HOST
# Start all services (Core + Web in parallel)
pnpm dev
# Core: tsx --watch apps/core/src/main.ts (port 4000)
# Web: vite apps/web (port 5173, proxied to :4000)Conclusion
GATOR Lite represents a complete redesign of the original GATOR system, optimized for personal single-user deployment. Key achievements include:
- Ultra-lightweight architecture (150MB RAM vs 720MB)
- Single-process design with embedded SQLite
- Hono framework for maximum performance
- Complete LSP-Bridge integration with Antigravity IDE
- Offline-first with IndexedDB queue
- Security-first with whitelist enforcement and network exposure guards
- Premium glassmorphism UI design
The system is designed for developers who want private, secure, ultra-fast remote control of their development environment from mobile devices.
(End of file - 605 lines)
Architecture Feedback
Spotted a potential optimization or antipattern? Let me know.