# Website backend (base)

The foundation for every backend package in the Runalio catalogue. Install it first.

It runs inside the website's own container as `node server.js` (Node 22, no npm dependencies) and gives:

- `server.js`: loads every file in `features/` and serves them on the port in `runalio.json`.
- `lib/http.js`: router, JSON, form and raw bodies, cookies, per-visitor rate limits, safe errors, `siteUrl(env)` and `apiUrl(env)`.
- `lib/db.js`: SQLite in `data/app.sqlite3` (WAL mode), `migrate()`, `transaction()`, IDs and tokens.
- `lib/admin.js`: the owner admin area at `<website>/api/admin`, with Runalio team sign-in, tables and CSV export for every package.
- `lib/jobs.js`: `every(app, name, seconds, fn)` for reminders and clean-ups. There is no system cron.
- `lib/email.js`: `sendEmail(app, {to, subject, text, html, replyTo})` through an HTTPS email API. SMTP is blocked.
- `lib/business.js`: shared contacts/activity, currency and integer price validation, capability links, paginated histories and versioned updates.
- `lib/console.js`: reusable owner forms and customer tracking pages with escaped content and current portal access.

## Install

Use the verified installer in the website container. It installs dependencies, checks checksums, and refuses to replace customer changes:

```sh
python /opt/runalio/catalog_install.py base
python /opt/runalio/catalog_install.py --status
```

Review the preview and click Publish to promote backend code and pages. Production data remains separate.

To undo the last installation before publishing: `python /opt/runalio/catalog_install.py --rollback`.

## Settings

The customer enters these in Runalio, Settings → Website modules → this module (never in files or chat):

| Name | Required | Secret | Purpose |
|---|---|---|---|
| `ADMIN_PASSWORD` | no | yes | Optional standalone admin passphrase. Managed Runalio websites use portal team access. |
| `EMAIL_PROVIDER` | no | no | resend, postmark, sendgrid or mailgun, for automatic emails. |
| `EMAIL_API_KEY` | no | yes | API key of that email provider. |
| `EMAIL_FROM` | no | no | Sender address on a domain verified with the email provider. |
| `MAILGUN_DOMAIN` | no | no | Mailgun sending domain (Mailgun only). |

## How requests reach it

Pages call the backend at the origin root: `fetch('/api/payments/config')`, including on nested pages. Runalio removes the `api/` prefix, so the
server sees `/payments/config`. Requests are same-origin; no CORS setup is needed.

- Always on for paid Runalio packages. On the free allowance it runs only while the customer edits (private preview).
- Logs: `/workspace/.runalio/backend.log`. Status: `/workspace/.runalio/backend.json`.
- The private preview restarts after coding changes. Publish promotes reviewed backend code and pages to your account’s EC2 instance. Production databases stay separate. Changing secrets in Settings restarts the approved live release.

## Writing a feature

Create `features/<name>.js` exporting `default function install(app)`:

```js
import { migrate, now } from '../lib/db.js';
import { rateLimit, requireString } from '../lib/http.js';
export default function install(app) {
  migrate(app.db, 'notes:1', 'CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL, created_at INTEGER NOT NULL)');
  app.router.post('/notes', (req) => {
    rateLimit(req, 'notes', 10, 3600);
    app.db.prepare('INSERT INTO notes (body, created_at) VALUES (?, ?)').run(requireString(req.body.body, 'Note', 500), now());
    return { ok: true };
  });
  app.adminSections.push({ id: 'notes', title: 'Notes', table: ({ db }) => ({ columns: ['Note'], rows: db.prepare('SELECT body FROM notes').all().map((n) => [n.body]) }) });
}
```

Handlers return a value (sent as JSON), throw `new HttpError(status, message)`, or write to `res` themselves.
Use `{ raw: true }` for signed webhooks and `{ limit: bytes }` to change the 64 KB body limit.

## What the customer needs to do

1. Open Settings → Website backend → Open business admin. Runalio checks current team membership on every request. `ADMIN_PASSWORD` is optional for standalone installations outside managed Runalio hosting.
2. Optional, for automatic emails: create an account with Resend, Postmark, SendGrid or Mailgun, verify the
   sending domain there, then add `EMAIL_PROVIDER`, `EMAIL_API_KEY` and `EMAIL_FROM`.
3. Choose a geographic hosting region before checkout. Starter uses a shared regional EC2 host with isolated account resources. Other paid plans use a dedicated EC2 instance for the account. Review the preview and click Publish to deploy backend changes.

## Check it

`curl -s localhost:3000/health` in the container lists installed features, configuration readiness and the preview/live mode. Configuration readiness does not claim a provider checkout or email delivery was tested.

Preview never sends email or SMS; use its outboxes to inspect proposed messages. Verified Stripe test payments remain separate from live totals. Production business operations such as dispatch, moderation and fulfillment update records immediately; agent source edits require Publish.
