Architecture overview

The backend is a single Node.js/Express app that does four things: handles API requests, runs AI tasks through Anthropic, dispatches work to a Bull queue (Redis), and processes Stripe billing events.

Architecture
Browser / Frontend HTML files
         │
         ▼
  Express API (Railway)          ← Your backend
         │
    ┌────┴────────────────────┐
    │                         │
Supabase DB            Anthropic API
(PostgreSQL)         (Claude Sonnet 4.6)
    │                         │
    └────────┐   ┌────────────┘
             │   │
          Bull Queue (Redis)    ← Async task processing
             │
        Email (Resend)  ←  Notifications
             │
         Stripe          ←  Billing webhooks

Prerequisites

You need accounts on four services — all have free tiers to get started.

1
Node.js 20+
Download from nodejs.org. Check version: node --version
2
Supabase account (free)
Sign up at supabase.com → Create a new project. Takes 2 minutes.
3
Anthropic API key
Get your key at console.anthropic.com/settings/keys. Add $5 credit to get started.
4
Stripe account (free)
Sign up at stripe.com. Test mode is free — no real money needed for testing.
5
Railway account (free trial)
Sign up at railway.app with GitHub. Free trial includes $5 credit — enough for weeks of testing.
6
GitHub account
Railway deploys from GitHub. Create a free account at github.com if you don't have one.

Local setup

Step 1 — Get the backend code

The backend folder panalo-backend/ is included with your project files. Put it on GitHub first:

Terminal
# Navigate to the backend folder
cd panalo-backend

# Initialize git
git init
git add .
git commit -m "Initial Panalo.ai backend"

# Create a new repo on github.com, then:
git remote add origin https://github.com/YOUR_USERNAME/panalo-backend.git
git branch -M main
git push -u origin main

Step 2 — Install dependencies

Terminal
cd panalo-backend
npm install

Step 3 — Create your .env file

Terminal
cp .env.example .env

Then open .env and fill in your keys (see Section 4 below).

Step 4 — Start the dev server

Terminal
npm run dev

# You should see:
# 🚀 Panalo.ai backend running { port: 3001, env: 'development' }
✅ Server running?
Open http://localhost:3001/health in your browser. You should see {"status":"ok",...}. That's your backend running locally.

Environment variables

Here's every variable you need, where to get it, and whether it's required to start.

VariableWhere to get itRequired?
JWT_SECRETMake up any random 32+ char string. Run: openssl rand -hex 32Required
JWT_REFRESH_SECRETAnother random string. Must be different from JWT_SECRET.Required
SUPABASE_URLSupabase Dashboard → Project Settings → API → Project URLRequired
SUPABASE_ANON_KEYSupabase Dashboard → Project Settings → API → anon/public keyRequired
SUPABASE_SERVICE_KEYSupabase Dashboard → Project Settings → API → service_role keyRequired
ANTHROPIC_API_KEYconsole.anthropic.com/settings/keysRequired
STRIPE_SECRET_KEYStripe Dashboard → Developers → API Keys → Secret key (use sk_test_... for testing)Required
STRIPE_WEBHOOK_SECRETStripe Dashboard → Developers → Webhooks → Signing secret (after you create a webhook)Required
STRIPE_PRICE_STARTERStripe Dashboard → Products → Create your 3 plans → copy Price ID (price_...)Required
REDIS_URLRailway provides this automatically when you add a Redis plugin. Leave blank for dev (tasks process inline).Optional
RESEND_API_KEYresend.com → API Keys. Free tier = 3,000 emails/month.Optional
FRONTEND_URLYour Vercel or Netlify URL, or http://localhost:3000 for local devRequired
⚠️ Never commit .env to GitHub
The .gitignore already excludes .env. Double-check before pushing. For Railway, you'll enter these as environment variables in the dashboard — never in code.

Database setup (Supabase)

Step 1 — Create a Supabase project

1
Go to supabase.com → New Project
Name it "panalo-ai". Choose a region closest to your users (US East for US clients).
2
Wait for the project to provision
Takes about 60 seconds. You'll see the project dashboard when it's ready.
3
Get your API keys
Settings → API. Copy Project URL, anon key, and service_role key into your .env file.

Step 2 — Run the database schema

Open the Supabase SQL Editor (left sidebar → SQL Editor → New Query) and paste this:

SQL — run in Supabase SQL Editor
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- USERS
CREATE TABLE IF NOT EXISTS users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL,
  first_name TEXT NOT NULL, last_name TEXT NOT NULL,
  company TEXT, role TEXT NOT NULL DEFAULT 'client',
  plan TEXT NOT NULL DEFAULT 'starter',
  subscription_status TEXT DEFAULT 'trialing',
  stripe_customer_id TEXT UNIQUE, stripe_subscription_id TEXT UNIQUE,
  subscription_renews_at TIMESTAMPTZ, task_count INTEGER DEFAULT 0,
  token_version INTEGER DEFAULT 0, last_login TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- AGENTS
CREATE TABLE IF NOT EXISTS agents (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES users(id) ON DELETE SET NULL,
  name TEXT NOT NULL, email TEXT UNIQUE NOT NULL,
  location TEXT DEFAULT 'Manila, PH', avatar_url TEXT,
  role TEXT DEFAULT 'agent', status TEXT DEFAULT 'offline',
  current_tasks INTEGER DEFAULT 0, completed_today INTEGER DEFAULT 0,
  avg_handle_time_minutes INTEGER DEFAULT 8, csat_score NUMERIC(3,2) DEFAULT 5.0,
  shift_start TIME, shift_end TIME,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- TASKS
CREATE TABLE IF NOT EXISTS tasks (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
  title TEXT NOT NULL, description TEXT,
  type TEXT DEFAULT 'other', complexity TEXT, priority TEXT DEFAULT 'normal',
  status TEXT DEFAULT 'pending', handler TEXT, tags TEXT[] DEFAULT '{}',
  due_date TIMESTAMPTZ, estimated_minutes INTEGER, ai_note TEXT,
  completed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at DESC);

-- TASK RESULTS
CREATE TABLE IF NOT EXISTS task_results (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  output TEXT NOT NULL, confidence INTEGER, steps_taken TEXT[],
  handler TEXT, agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
  rated INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- MESSAGES
CREATE TABLE IF NOT EXISTS messages (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  sender_id UUID REFERENCES users(id) ON DELETE SET NULL,
  sender_type TEXT NOT NULL, sender_name TEXT, body TEXT NOT NULL,
  read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_messages_task_id ON messages(task_id);

-- INVOICES
CREATE TABLE IF NOT EXISTS invoices (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  stripe_id TEXT UNIQUE, amount NUMERIC(10,2), currency TEXT DEFAULT 'usd',
  status TEXT DEFAULT 'paid', pdf_url TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- PASSWORD RESET TOKENS
CREATE TABLE IF NOT EXISTS password_reset_tokens (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token TEXT UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL,
  used BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- HELPER FUNCTIONS
CREATE OR REPLACE FUNCTION increment_agent_tasks(agent_id UUID)
RETURNS void LANGUAGE sql AS $$
  UPDATE agents SET current_tasks = current_tasks + 1, updated_at = NOW() WHERE id = agent_id;
$$;

CREATE OR REPLACE FUNCTION decrement_agent_tasks(agent_id UUID)
RETURNS void LANGUAGE sql AS $$
  UPDATE agents
  SET current_tasks = GREATEST(0, current_tasks - 1),
      completed_today = completed_today + 1, updated_at = NOW()
  WHERE id = agent_id;
$$;

Click Run. You should see "Success. No rows returned."

Step 3 — Seed test data

Terminal
npm run seed

# Output:
# ✅ Admin user: admin@panalo.ai / admin123!
# ✅ Test client: sarah@buildco.com / client123!
# ✅ 5 agents seeded
# ✅ 4 sample tasks seeded

Deploy to Railway (live URL)

Railway gives you a live HTTPS URL in about 3 minutes. Here's the exact process:

1
Click "Deploy from GitHub repo". Authorize Railway to access your GitHub account.
2
Select your panalo-backend repository
Railway detects it's a Node.js app and automatically configures the build. Click "Deploy Now".
3
Add Redis (for task queue)
In your Railway project → + New → Database → Add Redis. Railway auto-injects REDIS_URL into your app.
4
Add all environment variables
In your Railway service → Variables → paste all your .env values. Railway encrypts them at rest.
5
Get your live URL
Settings → Domains → Generate Domain. You'll get something like panalo-backend-production.up.railway.app
✅ Verify your deployment
Open https://YOUR-URL.up.railway.app/health
You should see: {"status":"ok","service":"panalo-ai-backend",...}
ℹ️ Auto-deploy on every push
Once connected, every git push to your main branch automatically deploys to Railway. You'll see build logs in real time in the Railway dashboard.

Stripe setup

Create your pricing plans

1
Click + Add Product.
2
Create three products
Starter ($49/month), Pro ($149/month), Enterprise ($499/month). For each, copy the Price ID (starts with price_) into your .env as STRIPE_PRICE_STARTER etc.

Set up the webhook

3
Stripe Dashboard → Developers → Webhooks → Add endpoint
Endpoint URL: https://YOUR-RAILWAY-URL.up.railway.app/api/webhooks/stripe
4
Select these events to listen for
checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.deleted
5
Copy the Signing Secret
After creating the webhook, click "Reveal" on the Signing Secret. Add it as STRIPE_WEBHOOK_SECRET in Railway.
💡 Test Stripe locally with the CLI
Install the Stripe CLI, then run:
stripe listen --forward-to localhost:3001/api/webhooks/stripe
This gives you a local webhook secret for dev testing.

Connect the frontend HTML apps

The HTML apps you've built need to point to your live backend URL. Open each HTML file and update the API base URL:

JavaScript — add to top of each HTML file's script block
// At the top of your <script> block in each HTML file:
const API_BASE = 'https://YOUR-RAILWAY-URL.up.railway.app/api';

// Replace all direct data with real API calls. Example:
async function loadTasks() {
  const token = localStorage.getItem('panalo_token');
  const res = await fetch(`${API_BASE}/tasks`, {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  const { tasks } = await res.json();
  renderTaskList(tasks);
}

// Auth example:
async function doLogin(email, password) {
  const res = await fetch(`${API_BASE}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  const { accessToken, refreshToken, user } = await res.json();
  localStorage.setItem('panalo_token', accessToken);
  localStorage.setItem('panalo_refresh', refreshToken);
  return user;
}
💡 Host the frontend on Vercel (free)
1. Install Vercel CLI: npm i -g vercel
2. Put your HTML files in a folder, then run: vercel --prod
3. Set FRONTEND_URL in Railway to your new Vercel URL
4. Add your Vercel URL to ALLOWED_ORIGINS in Railway

Live testing — step by step

Once deployed, test these endpoints in order to verify everything works end to end.

1. Health check

GET https://YOUR-URL.up.railway.app/health
Expected: {"status":"ok","service":"panalo-ai-backend"}

2. Register a test user

curl
curl -X POST https://YOUR-URL.up.railway.app/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@yourcompany.com",
    "password": "TestPass123!",
    "first_name": "Test",
    "last_name": "User",
    "plan": "pro"
  }'

Save the accessToken from the response — you'll use it for all other requests.

3. Submit a task (with real AI)

curl
curl -X POST https://YOUR-URL.up.railway.app/api/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -d '{
    "title": "Research top 3 CRM tools for a 20-person sales team",
    "description": "Need pricing, key features, and pros/cons for HubSpot, Salesforce, and Pipedrive",
    "priority": "normal"
  }'

Claude Sonnet 4.6 will attempt this task. If confidence ≥70%, it auto-completes. If not, it escalates to your agent queue.

4. Check task status

curl
curl https://YOUR-URL.up.railway.app/api/tasks/TASK_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

5. Test with Postman (recommended)

Download Postman for a visual way to test all endpoints. Import the collection by pointing at your base URL.

🎉 You're live!
If task creation returns a task object and the health check passes, your full stack is running: Express → Anthropic AI → Supabase → Queue → Email. Share your Railway URL with your team.

API routes reference

Auth — /api/auth

MethodPathDescriptionAuth
POST/api/auth/registerCreate account, get tokens
POST/api/auth/loginLogin, get tokens
POST/api/auth/refreshRefresh access token
POST/api/auth/logoutInvalidate tokensJWT
GET/api/auth/meGet current user profileJWT
POST/api/auth/forgot-passwordSend reset email
POST/api/auth/reset-passwordReset with token

Tasks — /api/tasks

MethodPathDescriptionAuth
GET/api/tasksList tasks (paginated, filterable)JWT
GET/api/tasks/:idGet task detail + AI result + messagesJWT
POST/api/tasksSubmit task → AI processes itJWT
PATCH/api/tasks/:idUpdate status/priorityJWT
DELETE/api/tasks/:idCancel a pending taskJWT
POST/api/tasks/previewAI preview for submit UIJWT
GET/api/tasks/stats/summaryDashboard statsJWT

Messages — /api/messages

MethodPathDescriptionAuth
GET/api/messages/:taskIdGet all messages for a taskJWT
POST/api/messages/:taskIdSend message (triggers email)JWT
GET/api/messages/inbox/allAll tasks with message threadsJWT

Billing — /api/billing

MethodPathDescriptionAuth
GET/api/billing/plansAll plan details and pricing
GET/api/billing/subscriptionCurrent plan + usageJWT
POST/api/billing/checkoutCreate Stripe checkout sessionJWT
POST/api/billing/portalStripe billing portal URLJWT
GET/api/billing/invoicesInvoice history from StripeJWT

Troubleshooting

Server won't start

Check that all required .env variables are set. The most common cause is a missing JWT_SECRET or SUPABASE credentials. Run node src/index.js directly to see the raw error.

Database errors on task creation

Make sure you ran the SQL schema in Supabase first. Go to Supabase → Table Editor — you should see users, tasks, messages, agents, task_results tables.

AI not responding

Check your ANTHROPIC_API_KEY is correct and has credit. Test it directly: curl https://api.anthropic.com/v1/messages -H "x-api-key: YOUR_KEY" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"claude-haiku-20240307","max_tokens":10,"messages":[{"role":"user","content":"Hi"}]}'

CORS errors in browser

Add your frontend URL to ALLOWED_ORIGINS in Railway. Example: http://localhost:3000,https://panalo.ai

Stripe webhook not firing

Make sure the webhook URL matches exactly: https://YOUR-URL/api/webhooks/stripe. Check Railway logs for the "Stripe webhook received" log line. In test mode, use the Stripe CLI to forward events locally.

View Railway logs

Railway CLI
# Install Railway CLI
npm install -g @railway/cli

# Login and view logs
railway login
railway logs
ℹ️ Need help?
All backend source files are in panalo-backend/. The main entry point is src/index.js. Routes are in src/routes/. AI logic is in src/lib/anthropic.js.

Railway docs: docs.railway.app
Supabase docs: supabase.com/docs
Anthropic docs: docs.anthropic.com