Platform
Platform Architecture
Last updated April 20, 2026
Norena is a fullstack TypeScript platform. The frontend handles the strategy builder, backtest engine, and UI. A separate backend runner process handles live execution. A shared Supabase database ties everything together.
System Overview
| Component | Technology | Responsibility |
|---|---|---|
| Frontend | Next.js 14 (App Router), React, TypeScript | UI, strategy builder, backtest engine (runs in-browser), user authentication |
| Execution Runner | Node.js, TypeScript (hornpub-runner) | Live strategy execution, kline data management, order placement, structured logging |
| Database | Supabase (PostgreSQL + RLS + Auth) | Persistent storage for all user data, projects, trades, logs, and wallets |
| Exchange | Binance REST API | Market data (klines) and order execution for live mode |
The frontend and runner are fully decoupled. The runner never calls the frontend and the frontend never calls the runner directly â they communicate only through the shared database. This means the runner can be deployed independently, scaled horizontally, or replaced without touching the frontend.
Frontend
The Next.js frontend handles everything the user sees and interacts with:
Google Blockly workspace. Custom block definitions, custom JavaScript generators, live compilation to JS with timeframe manifest injection.
Runs entirely client-side â no server round-trip. Fetches Binance klines directly, runs strategy in a Function() sandbox, computes 8 analytics modules.
Real-time view of project logs, positions, and trades via Supabase Realtime subscriptions.
Project configuration UI. Wallet management (encrypted credentials). All settings persisted to the database and read by the runner on each tick.
The frontend uses Next.js server components for data-fetching pages (authentication, initial project state) and client components for interactive elements (the Blockly editor, backtest charts, live log stream). Authentication is handled by Supabase Auth with server-side session management.
Execution Runner
The hornpub-runner is a standalone Node.js process â not part of the Next.js app. It runs independently and communicates with Supabase as its only external interface (apart from Binance for klines and orders).
Key responsibilities:
- Project scheduling â claims due projects via a database RPC function on a 2-second poll loop.
- Kline management â maintains an in-memory
KlineCachefed byInMemoryKlineManager, which fetches and refreshes data from Binance independently of project execution. - Strategy execution â runs compiled JS per-project, per-symbol in a
vmsandbox with the full strategy API surface. - Broker abstraction â
PaperBroker(simulated fills) andLiveBroker(Binance MARKET orders) share the same interface, making mode-switching transparent to strategy code. - Structured logging â writes TRADE_TRIGGER and TRADE_EXECUTED log pairs for every trade, with full condition rows and slippage data.
createIndicators() is used in both. This ensures that backtest results accurately reflect live execution behavior â same calculations, same data sources, same edge cases.Database
Norena uses Supabase (PostgreSQL) as its primary datastore. Supabase provides the database, row-level security policies, authentication, and realtime subscriptions â all in one managed service.
Row-level security is enforced at the database level so that every query â whether from the frontend's user session or the runner's service key â respects data ownership. Users can only access their own rows; the runner always scopes queries by owner_id.
Kline Data System
Live strategy execution uses an in-memory kline cache that is populated and maintained by the InMemoryKlineManager. This cache is completely separate from Supabase â market data never touches the database.
| Component | Role |
|---|---|
KlineCache | In-memory store. Holds the last N candles per symbol + interval. Provides O(1) series access by exchange/symbol/timeframe key. |
InMemoryKlineManager | Background process that queries Supabase for active project symbols, then fetches and continuously refreshes those series from Binance. Runs independently of project execution. |
| Binance REST API | Source of all kline data. Fetched directly â no intermediate caching layer. |
The manager discovers which symbols and timeframes to maintain by scanning active projects for their configured symbols and their compiled JS's timeframe manifests. When a new project goes live, its required series are automatically added to the refresh schedule.
In the backtest engine (frontend), klines are fetched on-demand directly from Binance when the user clicks Run Backtest. No cache is used â every backtest starts with a fresh fetch.
Scheduling
Project scheduling is managed through the database, not a cron job or external scheduler. Each project has a next_run_at timestamp. A PostgreSQL function (claim_due_projects) atomically selects and claims projects wherenext_run_at <= now() and updates their timestamp tonow() + interval_seconds in the same transaction.
This design provides several guarantees:
- Exactly-once claiming â no two runner instances can claim the same project simultaneously.
- No external scheduler dependency â the runner and database together are sufficient; no cron or queue service is needed.
- Catchup prevention â if the runner is briefly offline, projects will be claimed once when it restarts, not for every missed interval.
Data Model Overview
The core data model is intentionally small. These are the primary tables:
| Table | Description | Key fields |
|---|---|---|
users | Managed by Supabase Auth. Contains email, created_at, and auth metadata. | id (UUID), email |
projects | Each strategy project. Stores compiled JS, symbols, settings, and scheduling state. | id, owner_id, generated_js, symbols, settings_json, interval_seconds, next_run_at, status |
project_runs | One row per execution tick. Tracks run status, timing, and error summaries. | id, project_id, status, mode, started_at, finished_at |
project_positions | Open and closed positions per project + symbol. One open position max per pair. | id, project_id, symbol, status, entry_price, qty, close_price |
project_trades | Every buy and sell event. Linked to a position and to trigger/executed log rows. | id, project_id, side, symbol, fill_price, qty, fee, trigger_log_id, executed_log_id |
project_logs | All log entries: info, warn, error, and structured TRADE_TRIGGER/TRADE_EXECUTED entries with detail_json. | id, project_id, user_id, level, message, meta, detail_json |
wallets | Encrypted Binance API credentials per user. | id, owner_id, label, api_key_enc, api_secret_enc |
notifications | In-app notifications for trade events, errors, and system messages. | id, user_id, type, message, read, created_at |
There is no separate strategies table â strategy data (compiled JS, workspace JSON) lives directly on the projects row. This keeps the model simple and queries fast for the common case of reading a project to run it.