# Palbase — full documentation > Palbase — the backend for apps. Every project has auth, storage, realtime, > flags, notifications, secrets, analytics, AI and purchases, called from web > apps with the publishable key (`palbase/web`), from iOS (Swift) and Android > (Kotlin) with the native SDKs, and from servers with the secret key > (`palbase/server`). Your own backend runs on a machine built from a GitHub > branch on every push, beside a Postgres database of its own. Backends are > `palbase/backend`: `@Controller` classes whose methods are routes, > `@Injectable` services that arrive by constructor, a `@Module` that lists what > exists, `PalbaseFactory.create(AppModule)` in `src/main.ts`, and the schema > declared with Palgres in `src/db/public.ts`. A push to the branch is the > deploy; the CLI (`npx palbase`) does the rest — client apps, the purchase > catalog, environments, tokens. # Introduction Palbase is a backend platform in two halves. **Every project has a set of modules** — auth, storage, realtime, flags, notify, secrets and analytics — that an app uses with no backend of its own, the way it would use Firebase: sign users in, keep their files, push them messages, read flags, count events. And when a project needs code of its own, it runs on **machines** built from a GitHub branch on every push, beside **Postgres databases** of its own — with a TypeScript framework, `palbase/backend`, that is made for exactly that. You can use either half without the other. Most projects start with the modules from an app, and add a backend when a rule has to live somewhere an app cannot change it. ## A taste An app signs a user up with the project's publishable key: ```ts // app/signup.ts import { createClient } from "palbase/web"; const palbase = createClient({ url: "https://.palbase.studio", publishableKey: "pb__c…" }); const { user } = await palbase.auth.signUp({ email: "ada@example.com", password: "a passphrase of fifteen+" }); console.log(user.id); ``` A server — any server — sends that user an email with the secret key: ```ts // server/welcome.ts import { createServer } from "palbase/server"; const palbase = createServer({ url: process.env.PALBASE_URL!, secretKey: process.env.PALBASE_SECRET_KEY! }); await palbase.notify.sendEmail({ body: { to: "ada@example.com", subject: "Welcome", text: "Glad you are here." } }); ``` And a backend of your own is a class whose methods are routes, served on a machine: ```ts // src/modules/notes/notes.controller.ts import { Body, Controller, Database, Post, User, z, type UserT } from "palbase/backend"; const CreateNote = z.object({ title: z.string().min(1).max(200) }); type CreateNote = z.infer; @Controller("/notes") export class NotesController { // Every route needs a signed-in user unless it says otherwise; @User() is them. @Post("") async create(@Body(CreateNote) body: CreateNote, @User() user: UserT): Promise<{ id: string }> { const [row] = await Database.$query<{ id: string }>("insert into notes (owner, title) values ($1, $2) returning id", [user.id, body.title]); return row; } } ``` Push it to its branch, and the machine builds it and rolls it out beside the version that serves. The [Quickstart](/docs/getting-started/quickstart) does all three, end to end. ## What every project has | Module | What it does | From an app (publishable key) | From a server (secret key) | |---|---|---|---| | **auth** | Users, sessions, passwords, passkeys, second factors, magic links, phone codes, social sign-in, test users | [Web Auth](/docs/web/auth) | [Authentication](/docs/backend/authentication), [Test Users](/docs/backend/test-users) | | **storage** | Buckets and files, public reads, signed URLs, image renditions | [Uploads](/docs/web/uploads) — through a URL a server signed | [Storage](/docs/backend/storage), [Direct Uploads](/docs/backend/direct-uploads) | | **realtime** | Channels over one socket: broadcasts, shared state, presence; a backend's gateways | [Web Realtime](/docs/web/realtime), [Web Gateways](/docs/web/gateways) | [Realtime](/docs/backend/realtime), [Gateways](/docs/backend/gateways), [Channels](/docs/backend/channels) | | **flags** | Feature flags and remote config, per-user values | [Web Flags](/docs/web/flags) | [Flags](/docs/backend/flags) | | **notify** | Email, push, SMS and WhatsApp; templates, topics, preferences | a user's devices, preferences and topics | [Notifications](/docs/backend/notifications) | | **secrets** | The project's secrets, versioned, in two sets | — | [Secrets](/docs/backend/secrets) | | **analytics** | Events, identifies, page views; summaries, funnels and retention | [Web Analytics](/docs/web/analytics) | [Analytics](/docs/analytics/overview), [Reading Analytics](/docs/analytics/reading) | | **ai** | The platform's models: answers, tools, agents, and chats with the members a backend names | Through a backend's gateway: a chat's members join it ([Web Gateways](/docs/web/gateways#chat-gateways)) | [AI](/docs/backend/ai) | Each module answers at the project's address, under a path of its own (`/auth/…`, `/storage/…`, `/flags/…`), and each has a contract — the OpenAPI document the SDK's clients are generated from. The SDKs are a convenience over those paths, not a requirement: anything that speaks HTTPS can call them. And for code of your own: | | | |---|---| | **Machines** | Your program, built from a repository's branch and directory on every push; sizes that sleep and sizes that never do; logs, builds and rollback — [Machines](/docs/machines/overview) | | **Databases** | Postgres 18 with pgvector and PostGIS, backups and point-in-time restore, a read-only SQL console in Studio — [Databases](/docs/machines/databases) | | **`palbase/backend`** | Controllers, services and modules in TypeScript, secure by default, with the project's modules and its database built in — [Backend SDK](/docs/backend/overview) | ## The surfaces | Surface | What it is | Docs | |---|---|---| | `palbase/web` | The client for web and JavaScript apps: `createClient` with the publishable key — auth, the session, the realtime socket | [Web SDK](/docs/web/overview) | | `palbase/server` | The client for servers: `createServer` with a secret key — every module's server calls, and the receiver for the project's webhooks | [Server SDK](/docs/server/overview) | | `palbase/backend` | The framework a machine runs: `PalbaseFactory`, `@Module`, `@Controller`, and `Palbase`, `Database` and `Log` | [Backend SDK](/docs/backend/overview) | | `palgres` | The database half of the framework: the schema in TypeScript, typed queries, and migrations that run beside the version that serves | [Schema](/docs/backend/schema), [Schema changes](/docs/backend/migrations) | | Studio | Organizations, projects and environments; every module's screens; machines, databases, keys and secrets | [Organizations](/docs/getting-started/organizations) | Palbase has SDKs in TypeScript (`palbase/web`, `palbase/server`, `palbase/backend`), Swift and Kotlin. An iOS app takes the Swift SDK, `palbase-swift` with Swift Package Manager ([iOS SDK](/docs/ios/overview)), and an Android app the Kotlin one, `studio.palbase:palbase` and an artifact per module ([Android SDK](/docs/android/overview)). An app on another platform, such as a game engine, calls the modules over HTTPS with the publishable key in the `apikey` header and the user's access token as `Authorization: Bearer …`, the same requests the SDKs make. The TypeScript SDKs are one npm package, `palbase`, whose parts are its subpaths — the CLI comes with it (`npx palbase`) — and `palgres`, which `palbase/backend` brings: `npm install palbase` (or `bun add palbase`). They ship compiled, under the Palbase SDK License ([Project Structure](/docs/getting-started/project-structure)). ## Organizations, projects and environments An **organization** is a team, a **project** is an app in it, and an **environment** is the thing that runs: each one a project of the platform's own, with **nothing shared** with another environment — its own address, its own keys, its own users and files, its own machines and databases. A staging copy of production is a second environment, with its machines built from another branch. Everyone in the organization reaches its projects at their role, owner, admin or member: members, invitations, roles and the audit log are in [Organizations](/docs/getting-started/organizations). A project's **first environment** has the slug `main`. Every environment answers at one address, named by its **ref**: ```text https://.palbase.studio ``` The ref is on the environment's **Settings**, and it never changes. A machine of the environment answers under the app domain — `https://.app.palbase.studio` for the one named `backend` — and a database at `-.db.palbase.studio`. ## The keys An environment has **publishable keys** — each [registered app](/docs/apps/overview)'s own, the one the environment was made with, and any you make — and **any number of secret keys**, on **Settings › API keys**: | Key | Looks like | Role | Where it goes | |---|---|---|---| | Publishable | `pb__c…` | `anon` | In apps: a browser bundle, a mobile app. What it reaches is what an app may do, and the signed-in user's token decides whose data. | | Secret | `pb__s…` | `service_role` | On servers only: a machine's secrets, a server's environment. It opens the whole project — every user, every file, every secret. | The ref is in the key, so a key names its project. A secret key is made with a name (**Create secret key**), **shown once**, and revoked on its own (**Revoke**) — give each server its own, and a leaked one is replaced without touching the others. `createClient` refuses a secret key, and `createServer` refuses to run in a browser. Every request to the project carries one of them in the `apikey` header; a request with none is `401 missing_api_key`, and a key of another project is the same as none (`401 invalid_api_key`). A user's access token, when one is sent, must be one this project issued (`401 invalid_token` otherwise). Past the project's requests-per-second limit, a request is `429 rate_limited` with `Retry-After: 1`; past its requests for the period, under a quota that refuses, `429 quota_exceeded` with `Retry-After` until the next period. > **Note:** The project's address answers a browser on any site. A browser app calls it directly, with the publishable key and the signed-in user's token, as [Web SDK](/docs/web/overview#calling-the-project-from-a-browser) explains. Your own machine's backend answers cross-origin requests itself, for the origins it names. ## How code reaches a machine One way: **a push to the machine's branch.** 1. [Connect GitHub](/docs/machines/github) — install **Palbase Cloud** on the account that owns the repository, and connect it to the environment. 2. Open a machine on a repository, a branch and a directory of it (**Backend › Machines › New machine**). 3. Push. A push that changes the machine's directory is built — its `Dockerfile`, or what Railpack detects — and rolled out beside the version that serves; it takes traffic once it answers its health check. There is no deploy command and no tool to install. A release whose machine migrates a database runs the migration itself, beside the version that serves; a release that does not come up never takes a request; and any build that succeeded can be returned to ([Builds, Logs & Rollback](/docs/machines/builds)). ## Related - [Quickstart](/docs/getting-started/quickstart) — a web app, a backend, a machine, end to end - [Architecture](/docs/getting-started/architecture) — how a backend's code is shaped - [Project Structure](/docs/getting-started/project-structure) — what a backend repository holds - [Web SDK](/docs/web/overview) · [Server SDK](/docs/server/overview) · [Backend SDK](/docs/backend/overview) - [Machines](/docs/machines/overview) · [Databases](/docs/machines/databases) # Quickstart In this guide you create a Palbase project in Studio, sign a user up and in from a web app, write a small backend with `palbase/backend`, put it on a machine by pushing it to GitHub, and call it from the web app as the signed-in user. Nothing here is run from a command line of Palbase's — the project is Studio's, the deploy is `git push`. ## Before you start | You need | Why | |---|---| | **A Studio account** | where the project lives — Palbase is opening step by step: [request access](/signup) | | **A GitHub repository** you can push to | a machine is built from a branch of it | | **Bun 1.4** — [bun.sh](https://bun.sh) | the backend runs on Bun, and `bun install` writes the lockfile its build installs from | | **Node.js 22** | the web app (Next.js) | > **Note:** The SDK is one npm package, `palbase` — `palbase/web` for the app, `palbase/backend` for the backend (it brings `palbase/server` and `palgres`), and the CLI (`npx palbase`): `npm install palbase` (or `bun add palbase`). ## 1. Create a project In Studio, **Projects › Create project**: a **Project name**, its **First environment**, and the **Organization** it belongs to (asked only when you have more than one). The first environment's slug is always `main`. Open the environment. Everything in this guide is that environment's — it is a project of its own, with its own address, keys, users, machines and databases: - its **address**, `https://.palbase.studio` — the ref is on **Settings**; - its **publishable key**, `pb__c…`, on **Settings › API keys** — the key apps ship. ## 2. Sign a user up from a web app Start a Next.js app and give it the web SDK: ```json { "dependencies": { "palbase": "^0.10.0", "next": "^16.3.4", "react": "^19.2.7", "react-dom": "^19.2.7" } } ``` and three settings, in `.env.local`: ```text NEXT_PUBLIC_PALBASE_URL=https://.palbase.studio NEXT_PUBLIC_PALBASE_PUBLISHABLE_KEY=pb__c… NEXT_PUBLIC_BACKEND_URL=https://.app.palbase.studio ``` **A browser calls the project's address directly.** The address answers a page on any site, so nothing stands between the app and the project ([Web SDK](/docs/web/overview#calling-the-project-from-a-browser) says what it answers, and why the browser is the one to call it). Next.js needs no setting for the SDK: the package is compiled JavaScript. The client is made once, in the browser, with the project's address: ```ts // lib/palbase.ts import { createClient, type PalbaseClient } from "palbase/web"; let client: PalbaseClient | undefined; /** The project's client: its publishable key, and the signed-in user's session. */ export function palbase(): PalbaseClient { client ??= createClient({ url: process.env.NEXT_PUBLIC_PALBASE_URL!, publishableKey: process.env.NEXT_PUBLIC_PALBASE_PUBLISHABLE_KEY!, }); return client; } ``` And a page that signs up, signs in, and — after step 6 — calls the backend: ```tsx // app/page.tsx "use client"; import { useState } from "react"; import { PalbaseError } from "palbase/web"; import { palbase } from "../lib/palbase"; export default function Home() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [said, setSaid] = useState(""); async function attempt(work: () => Promise) { try { setSaid(await work()); } catch (e) { setSaid(e instanceof PalbaseError ? `${e.status} ${e.code}: ${e.message}` : String(e)); } } const signUp = () => attempt(async () => { const { user } = await palbase().auth.signUp({ email, password }); const session = await palbase().auth.currentSession(); return session ? `signed up and in as ${user.id}` : `signed up as ${user.id}: verify the address, then sign in`; }); const signIn = () => attempt(async () => { const result = await palbase().auth.signIn(email, password); return result.user ? `signed in as ${result.user.id}` : "this account asks for a second factor"; }); const callBackend = () => attempt(async () => { const session = await palbase().auth.currentSession(); if (!session) return "sign in first"; const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/me`, { headers: { authorization: `Bearer ${session.accessToken}` }, }); return `${res.status} ${await res.text()}`; }); return (
setEmail(e.target.value)} placeholder="email" /> setPassword(e.target.value)} placeholder="password (15+ characters)" type="password" />

{said}

); } ``` Run it (`npm run dev`), sign up, and the user is in **Auth › Users** in Studio. A password of a single factor is 15 characters at least. A project that asks for a verified email answers the sign-up without a session; the verification is [Web Auth](/docs/web/auth)'s to explain. You have used one module — auth — with no backend at all. Storage, realtime, flags, notify and analytics are the same: every project has them, the publishable key reaches what an app may do, and the secret key reaches the rest ([Introduction](/docs/getting-started/introduction)). ## 3. Write the backend A backend is its own directory — its own repository here — that a machine builds: ```text notes-backend/ ├── Dockerfile ├── .env.example ├── package.json ├── bun.lock ├── tsconfig.json └── src/ ├── main.ts ├── app.module.ts ├── me.controller.ts └── modules/notes/ ├── dto.ts ├── notes.controller.ts ├── notes.module.ts └── notes.service.ts ``` ```json { "name": "notes-backend", "private": true, "type": "module", "scripts": { "start": "bun run src/main.ts" }, "dependencies": { "palbase": "^0.10.0" } } ``` The decorators read the types TypeScript emits, so `tsconfig.json` turns both on: ```json { "compilerOptions": { "target": "es2023", "module": "esnext", "moduleResolution": "bundler", "strict": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "allowImportingTsExtensions": true, "noEmit": true, "skipLibCheck": true }, "include": ["src/**/*.ts"] } ``` The schemas of a domain sit in its `dto.ts` — each one a value for `@Body` and a type of the same name: ```ts // src/modules/notes/dto.ts import { z } from "palbase/backend"; export const CreateNote = z.object({ title: z.string().min(1).max(200) }); export type CreateNote = z.infer; export const Note = z.object({ id: z.string(), owner: z.string(), title: z.string(), created_at: z.string() }); export type Note = z.infer; ``` The service holds the logic. This one keeps notes in the machine's memory — enough to see the whole path work; [step 8](#8-where-next) gives it a database: ```ts // src/modules/notes/notes.service.ts import { Injectable, NotFound } from "palbase/backend"; import type { CreateNote, Note } from "./dto"; // In this process only: a machine that sleeps, or a second copy, does not have them. @Injectable() export class NotesService { private readonly notes = new Map(); list(owner: string): Note[] { return [...this.notes.values()].filter((n) => n.owner === owner); } create(owner: string, input: CreateNote): Note { const note: Note = { id: crypto.randomUUID(), owner, title: input.title, created_at: new Date().toISOString() }; this.notes.set(note.id, note); return note; } get(owner: string, id: string): Note { const note = this.notes.get(id); if (!note || note.owner !== owner) throw new NotFound(`no note ${id}`); return note; } } ``` The controller is HTTP only — and every route needs a signed-in user unless it says otherwise: ```ts // src/modules/notes/notes.controller.ts import { Body, Controller, Get, Param, Post, User, type UserT } from "palbase/backend"; import { CreateNote, type Note } from "./dto"; import { NotesService } from "./notes.service"; @Controller("/notes") export class NotesController { constructor(private readonly notes: NotesService) {} @Get("") list(@User() user: UserT): Note[] { return this.notes.list(user.id); } @Post("", { rateLimit: { max: 30, window: 60 } }) create(@Body(CreateNote) body: CreateNote, @User() user: UserT): Note { return this.notes.create(user.id, body); } @Get("/{id}") read(@Param("id") id: string, @User() user: UserT): Note { return this.notes.get(user.id, id); } } ``` ```ts // src/modules/notes/notes.module.ts import { Module } from "palbase/backend"; import { NotesController } from "./notes.controller"; import { NotesService } from "./notes.service"; @Module({ controllers: [NotesController], providers: [NotesService] }) export class NotesModule {} ``` `/me` answers who the caller is: ```ts // src/me.controller.ts import { Controller, Get, User, type UserT } from "palbase/backend"; @Controller("/me") export class MeController { @Get("") me(@User() user: UserT): { id: string; email: string | null; emailVerified: boolean } { return { id: user.id, email: user.email, emailVerified: user.emailVerified }; } } ``` The root module lists what exists, and `main.ts` serves it. The browser calls the backend from the web app's origin, so the backend names that origin (CORS is the backend's own to answer): ```ts // src/app.module.ts import { Module } from "palbase/backend"; import { MeController } from "./me.controller"; import { NotesModule } from "./modules/notes/notes.module"; @Module({ imports: [NotesModule], controllers: [MeController] }) export class AppModule {} ``` ```ts // src/main.ts import { PalbaseFactory } from "palbase/backend"; import { AppModule } from "./app.module"; // Serves on $PORT. The project's address, id and secret key come from the // environment: the machine is started with them. await PalbaseFactory.create(AppModule, { cors: { origins: (process.env.WEB_ORIGINS ?? "http://localhost:3000").split(",") }, }); ``` How the machine runs it: ```dockerfile FROM oven/bun:1.4.2-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f WORKDIR /app COPY package.json bun.lock ./ RUN bun install --frozen-lockfile --production COPY tsconfig.json ./ COPY src ./src USER bun CMD ["bun", "run", "src/main.ts"] ``` And what it needs before it can start — the build reads this file, and waits rather than roll out an app that would not: ```text PALBASE_URL= PALBASE_PROJECT_ID= PALBASE_SECRET_KEY= ``` Run `bun install` once to write `bun.lock`, and commit everything. ## 4. Give it the secret key The machine is started with `PALBASE_URL` and `PALBASE_PROJECT_ID` by the platform. `PALBASE_SECRET_KEY` is yours to give — the backend uses it to reach the project's modules and to check its callers' sessions. 1. **Settings › API keys › Secret keys**: a **Key name** (`notes backend`), **Create secret key**. It is shown **once** — copy it now. 2. **Environment › Variables**, **Add secret**: the name `PALBASE_SECRET_KEY`, the key as its value. The secret is in the environment's **production** set. A machine starts with the set it names, which you choose in the next step. ## 5. Connect GitHub and open the machine **Backend › Machines › New machine**: 1. **Connect GitHub** — GitHub asks where to install **Palbase Cloud**; choose the repository, and connect it to this project by writing its ref when Palbase asks. Back in Studio, **Refresh** ([Connecting GitHub](/docs/machines/github)). 2. **Repository**: `you/notes-backend`; **Branch**: `main`; **Directory**: empty (the repository's root). 3. **Name**: `backend` — the machine of that name answers at `https://.app.palbase.studio`. **Size**: `shared-256`. **Port**: `8080`. 4. **Create machine**. The branch's head is built at once. Its build stops at **Waiting for variables**, `PALBASE_SECRET_KEY` missing: the machine starts with no secrets yet. Open it, **Edit settings**, **Secrets: Production secrets**, **Save settings** — and the waiting build rolls out by itself. `.env.example` is what caught it ([Builds](/docs/machines/builds#envexample-what-the-program-must-be-given)). ## 6. Push From now on, a push is the deploy: ```bash git commit -am "Notes: read one by id" git push origin main ``` The commit gets a **Palbase Cloud** check on GitHub — *Building*, then *Rolled out* — and the machine page lists the build with its log. The new version starts beside the old one and takes traffic once it answers its health check; a version that does not come up never takes a request. ## 7. Call it Without a token, the backend refuses — in the envelope every refusal comes in: ```bash curl -i https://.app.palbase.studio/me # HTTP/2 401 # {"error":"unauthorized","error_description":"This route needs a signed-in user","status":401,"request_id":"req_…"} ``` From the web app, signed in, **Call the backend** answers `200` with the user's id and email: the browser sent the session's access token, and the backend checked it against the project. Notes work the same way — `POST /notes` with `{"title": "…"}` and the same header. What the backend writes is on its machine page under **Logs**, and `Log` from `palbase/backend` puts the request's id on every line ([Builds, Logs & Rollback](/docs/machines/builds#logs)). Every request to the environment's address, whichever module answers it, is on Studio's **Logs** page ([Logs](/docs/getting-started/logs)). ## 8. Where next - **A database.** **Database › Databases › New database**, then give it to the machine with **Run migrations** on. Declare the table in `src/db/public.ts`, pass it to `PalbaseFactory.create(AppModule, { db: { schemas: [schema] } })`, and the next push migrates it before the new version serves: [Databases](/docs/machines/databases), [Database](/docs/backend/database), [Schema](/docs/backend/schema). - **Who sees what.** Row-level security in the schema keeps each user's rows theirs, whatever a route forgets: [Row-Level Security](/docs/backend/row-level-security). - **The other modules** from the backend — files, email, flags, realtime — are `Palbase.storage`, `Palbase.notify`, `Palbase.flags`, `Palbase.realtime`: [Storage](/docs/backend/storage), [Notifications](/docs/backend/notifications). - **More environments.** A staging environment is another environment of the project, with machines built from another branch. ## Related - [Introduction](/docs/getting-started/introduction) — the modules, the keys, and environments - [Architecture](/docs/getting-started/architecture) — how a backend's code is shaped - [Project Structure](/docs/getting-started/project-structure) — every file of the backend above - [Machines](/docs/machines/overview) · [Connecting GitHub](/docs/machines/github) · [Builds, Logs & Rollback](/docs/machines/builds) - [Web SDK](/docs/web/overview) · [Web Auth](/docs/web/auth) — the client in full - [Calling Your Backend](/docs/web/backend-calls) — the browser side of step 7 # Organizations Studio has three levels. An **organization** is a team: its people and their roles, its access tokens and its audit log. A **project** is an app, in one organization. An **environment** is what runs: each one a Palbase project of its own, with its own address, keys, users, files, machines and databases, and nothing shared with another. ```text Organization its members and their roles, access tokens, the audit log └── Project an app └── Environment main, staging, a developer's own: address, keys, users, files, flags, machines, databases ``` Everyone in an organization reaches every project and environment of it, at their role in the organization. There is no role per project or per environment. ## Organizations Before your first project, Studio asks you to create your organization, and you are its **owner**. Any account makes more: ```sh palbase orgs:create "Acme" palbase orgs:list # your organizations and your role in each ``` An organization's **Projects** page lists its projects, and an admin renames it under **General**. ## Projects and environments An admin creates a project, in Studio (**New project**) or from the CLI; it opens with its first environment: ```sh palbase projects:create shop --org acme # its first environment, main, and its keys palbase envs:create staging -P shop # a second environment palbase envs:create staging -P shop --from main # opened with main's module settings ``` - **The first environment's slug is `main`**, whatever it is called on screen: it is the project's production one, and a CLI change to it asks for confirmation. - **Another environment's slug** starts with a letter and continues with up to 38 letters, digits or hyphens; `local` is not one. An environment's name on screen can change; its slug never does. - **`--from`** opens the new environment with another's module settings: secrets by name, not by value. With `--with-cloud` its machines and databases are declared alike too, built from the same branches, with no data. Each environment has its own address and keys ([Introduction](/docs/getting-started/introduction#the-keys)), and its own usage ([Usage & Limits](/docs/getting-started/usage-and-limits)). An admin deletes an environment under its **Settings › Danger zone**, or with `palbase envs:delete `; deleting a project, under its **Settings › Delete project** or with `palbase projects:delete `, deletes every environment of it. An environment's users, files, flags, keys and secrets go with it, and its machines and databases are deleted too. ## Members and invitations An organization's **Members** page lists its people, each with their role and when they joined; its admins also see the invitations still open. An admin invites someone by email, as an **admin** or a **member** (member unless said), with **Add member** or from the CLI: ```sh palbase orgs:members:invite ada@example.com --role admin --org acme palbase orgs:members:list --org acme palbase orgs:members:remove ada@example.com --org acme # a member, or an invitation still open ``` - **Accepting.** The invited person signs in to Studio with the email the invitation names, and accepts it there. The email must be verified: an account whose address is not is refused (`403 email_not_verified`). An invitation does not expire; it waits until it is taken or withdrawn. - **Inviting again** keeps the invitation, with the new role and the new inviter. - **The mail.** An invitation is mailed from Palbase when the inviter's own email is verified. An address gets one invitation mail a day from an organization and five a day from all of them together; past that the invitation still waits for it in Studio, unmailed. - **How many.** An inviter makes 20 invitations an hour and 100 a day, and an organization 20 an hour, however many admins it has (`429` with `Retry-After`). A member leaves an organization with `palbase orgs:members:remove` and their own email. An admin takes away a member or an admin; only an owner takes away an owner. The organization's last owner stays: make another owner first (`409 last_owner`). A person who leaves, or is taken away, takes with them what they held at the organization's environments: the CLI's keys they were given, and the access tokens they made. - **Owners hear of every new role.** When someone — or an access token — is given a role in one of the organization's environments, or a higher one, each owner is mailed: who, which role, which environments, when. An owner whose own owner role is taken is mailed too. The same person and role is mailed to an owner once a day for an environment, the environments it came in named in one mail. The mail comes from Palbase's platform itself, not from Studio, to the address the owner signs in with: if you did not expect it, take the person away under **Members** and write to security@pallasite.net. ## Roles | | Member | Admin | Owner | |---|:---:|:---:|:---:| | Open every project and environment, and read its screens: users, files, flags, logs, usage, machines and their builds | ✓ | ✓ | ✓ | | See which secrets there are and their versions, and the API keys by their last characters | ✓ | ✓ | ✓ | | Read with a module's questions that change nothing: a funnel, a retention table, the database's read-only SQL console, a machine's `GET` from **Try it** | ✓ | ✓ | ✓ | | Change anything in an environment: settings, users, flags, files, templates, machines, databases, secrets | | ✓ | ✓ | | Read a secret's value, a task's payload, a coupon's one-time codes | | ✓ | ✓ | | Make and revoke API keys; take a CLI key (`palbase env:pull`, `palbase run`); mint environment tokens | | ✓ | ✓ | | Create, rename and delete projects and environments | | ✓ | ✓ | | Invite and take away members and admins; withdraw invitations; rename the organization | | ✓ | ✓ | | Manage access tokens; read the audit log | | ✓ | ✓ | | Make or unmake an owner; take away an owner | | | ✓ | A member's change to an environment is refused with `403 console_role_required`, and the refusal is kept in the audit log. In Studio, an owner changes a member's role from the **Members** list. A role is read on every request: a member made less than an admin keeps none of the CLI keys they had at the organization's environments, and the admin tokens they made stop working at once. An access token acts at its role, member or admin and never owner, and no more than its maker's role now ([Access Tokens](/docs/getting-started/access-tokens)). ## Your Studio account **Account security**, in the account menu at the top right, is your own account's: - **Two-factor authentication**: a second step when you sign in, with an authenticator app or by SMS, and **recovery codes** for when you have neither. They are shown once: keep them where you keep passwords. - **Passkeys**: sign in with this device's fingerprint, face or screen lock, or a security key, instead of a password. Add one, rename it, or remove it. - **Connected accounts**: sign in with Google or GitHub, when Studio offers them. The last way you sign in cannot be removed: add a password or a passkey first. A change to how you sign in needs a sign-in from the last five minutes; an older one is asked to sign in again first. Turning off a second factor asks for your current password. The CLI signs in as you, and holds a session of yours ([CLI](/docs/cli/overview#signing-in)). ## The audit log An organization's **Audit log** says who changed what at its environments, newest first: the change, the environment, who made it — a member by their email, an access token or an environment token by its name — the method and path, and the status it was answered. It holds every change made through Studio, the CLI and tokens; a change refused for its role; and every read of a secret's value. The path is kept without its query and the body not at all, since what was sent can hold a secret. Plain reads are not kept. Its admins read it in Studio, or from the CLI: ```sh palbase orgs:audit --org acme --limit 100 ``` A module keeps a change made through Studio as Studio's: the audit log is where whose it was is kept. See [`palbase orgs:audit`](/docs/cli/reference#palbase-orgsaudit). ## Related - [Access Tokens](/docs/getting-started/access-tokens) — for CI and agents, at a role of the organization - [Usage & Limits](/docs/getting-started/usage-and-limits) — what each environment uses - [CLI](/docs/cli/overview) — signing in, and choosing an environment - [CLI reference](/docs/cli/reference#orgs) — `orgs`, `projects` and `envs` # Architecture A Palbase backend is a plain TypeScript repository, and **one declaration says what exists, who owns it and what it may reach**: a `@Module` lists a domain's classes — its `controllers`, its `providers`, what it `exports` and whose exports it `imports` — and a class that no module lists is **refused at boot**, by name. The root module is the one `src/main.ts` hands to `PalbaseFactory.create`; every other module is reached from it through `imports`. Dependencies arrive through the **constructor**, and the container supplies them: a dependency is named by its parameter's **type** and by nothing else. There is no `@Inject`, no `useFactory`, no `forwardRef` and no token registry. What the platform gives every handler — the project's modules, the request's database, the log — is **imported**, never injected: `Palbase`, `Database` and `Log` from `palbase/backend`. This page answers one question: **what shape does the code take?** It is the shape of Palbase's own examples and of Studio's backend; [Project Structure](/docs/getting-started/project-structure) lists the files, and [Modules & Dependency Injection](/docs/backend/modules) the full declaration surface. ## The layers | Layer | What it does | What it never does | |---|---|---| | `src/main.ts` | Boots the app: `PalbaseFactory.create(AppModule, config)` — the database's schemas, CORS, the routes' default auth and limit | holds routes or logic | | `src/app.module.ts` | The root: `imports` every domain's module | owns a domain's classes | | `src/modules//.module.ts` | The declaration: which classes the domain owns, and which of them another domain may reach | holds logic · lists a class another module owns | | `src/modules//.controller.ts` | HTTP only: validates input through a schema, picks the caller, turns the service's answer into the response. Listed in `controllers` | imports `Database` · holds business rules · keeps per-request state on the instance | | `src/modules//.service.ts` | The decisions: which rows, whose, in what order; the calls to the project's modules. `@Injectable()`, listed in `providers` | knows about HTTP (no request, no response) · builds its own dependencies | | `src/modules//dto.ts` | Schemas — each a **value** for `@Body` and a same-named **type** | holds logic | | `src/db/public.ts` | **Is** the database: tables, columns, indexes, [row-level security](/docs/backend/row-level-security) | produces migration files — there are none | In one line: **controllers thin, services thick, schemas quiet, `db/` the single truth — and the module is the registration.** A domain is a **folder** with a module in it, and adding one is adding a folder and naming its module in the root's `imports`. The file suffixes (`.controller.ts`, `.service.ts`) are a convention that helps a reader and decides nothing: what decides is the list a class appears in and the decorator it carries. The rule that carries the most weight is the narrowest: **a controller does not import `Database`.** When a route starts reaching for a table, the logic has moved into the layer that is hardest to test, and the layer that was meant to hold it becomes decoration. ## Where does this go | The need | Its home | |---|---| | Validate input | a schema in the module's `dto.ts`, passed to `@Body(Schema)` / `@QueryParams(Schema)` | | Enforce ownership | a row-level-security policy in `src/db/public.ts` **and** the service's `where` — the policy is the backstop, not a reason to stop writing the filter | | A business rule, a calculation, an ordering | the service | | Answer with an HTTP status | `throw new NotFound(…)` (or `Forbidden`, `Conflict`, `PalError`) — from the service or the controller; no response object is needed | | Call another domain | name its class in your constructor — its module `exports` it, and yours `imports` that module | | Send an email, sign a file URL, read a secret | `Palbase.notify`, `Palbase.storage`, `Palbase.secrets` — in the service | | Work at start-up or shutdown | an `onStart()` / `onShutdown()` method on a provider — awaited in dependency order | | Work on a schedule | a [cron](/docs/backend/cron) — not a timer: a machine that sleeps runs none | | Work done once, now or later | a [task](/docs/backend/tasks) on the machine's queue — not a cron that looks in a table every minute | | Register a new class | add it to a module's `providers` (or `controllers`) — nothing else registers anything | | A setting | an environment variable of the machine; a secret is a project secret the machine starts with | ## One vertical, end to end A `notes` domain, in the order you would build it. ### 1. The database ```ts // src/db/public.ts import { defineSchema, defineTable, index, policy, text, timestamp, uuid } from "palbase/backend"; // Each user's notes, theirs alone: row-level security says so, whatever a route forgets. const notes = defineTable("notes", { columns: { id: uuid().primaryKey().defaultRandom(), owner: text(), title: text(), created_at: timestamp().defaultNow(), }, policies: (p) => [ policy("notes_own").for("all").to("authenticated").using(p.col("owner").eq(p.auth.uid())).withCheck(p.col("owner").eq(p.auth.uid())), ], indexes: (p) => [index("notes_owner_created").on(p.col("owner"), p.col("created_at"))], }); export default defineSchema("public", { tables: [notes] }); ``` One file per schema, each with a default export. There are no migration files: a release migrates the database to this declaration beside the version that serves ([Schema changes](/docs/backend/migrations)). The request runs as its caller — `authenticated`, with the token's claims — so `auth.uid()` is the signed-in user, and the policy holds for every query of the request. > **Note:** `ownedByUser()` is the column type made for `owner`: when the user is deleted, their rows go. The platform tells the app through the project's webhook (`user.deleted`), so an app whose schema has an `ownedByUser()` or `userRef()` column does not boot without `PALBASE_WEBHOOK_SECRET`, the secret of the project's webhook endpoint — and an endpoint cannot be made from Studio. A project without one keeps its owner column a `text()` with a policy, as here. See [Event Hooks](/docs/backend/hooks). ### 2. The schemas ```ts // src/modules/notes/dto.ts import { z } from "palbase/backend"; export const CreateNote = z.object({ title: z.string().min(1).max(200) }); export type CreateNote = z.infer; export const ListNotes = z.object({ limit: z.coerce.number().int().min(1).max(100).default(20) }); export type ListNotes = z.infer; export const Note = z.object({ id: z.string(), title: z.string(), created_at: z.string() }); export type Note = z.infer; ``` Each schema is exported twice under one name: the **value** a decorator validates with, and the **type** a signature reads. Any Standard Schema works — zod, valibot, arktype; `z` is re-exported from `palbase/backend` so the app and the framework agree on one zod. ### 3. The service ```ts // src/modules/notes/notes.service.ts import { Database, Injectable, Log, NotFound } from "palbase/backend"; import type { CreateNote, Note } from "./dto"; @Injectable() export class NotesService { async list(owner: string, limit: number): Promise { // RLS already narrows this to the caller's rows; the filter says what the route means. return Database.public.notes.findMany({ where: { owner }, select: ["id", "title", "created_at"], orderBy: { column: "created_at", direction: "desc" }, limit, }); } async create(owner: string, input: CreateNote): Promise { const note = await Database.public.notes.insert({ owner, title: input.title }); Log.info("note created", { note: note.id }); return { id: note.id, title: note.title, created_at: note.created_at }; } async remove(owner: string, id: string): Promise { const gone = await Database.public.notes.deleteMany({ where: { id, owner } }); if (gone === 0) throw new NotFound(`no note ${id}`); } } ``` `Database` is **the request's own transaction**: it is opened by the first query, committed when the handler returns — before the answer goes — and rolled back when it throws. A service the handler calls shares it, so a route's writes land together or not at all. It is reached inside a request (a handler, a service it calls, an `@On` handler), never at import time or from a timer. `Database.public.notes` is typed by the declaration file Palgres writes from `src/db/public.ts` ([Database](/docs/backend/database)): a column that does not exist, or a value of the wrong type, is a compile error. Raw SQL is `Database.$query(sql, params)`. ### 4. The controller ```ts // src/modules/notes/notes.controller.ts import { Body, Controller, Delete, Get, Param, Post, QueryParams, User, type UserT } from "palbase/backend"; import { CreateNote, ListNotes, type Note } from "./dto"; import { NotesService } from "./notes.service"; @Controller("/notes") export class NotesController { // The container supplies this. Nothing here constructs or imports an instance. constructor(private readonly notes: NotesService) {} @Get("") list(@QueryParams(ListNotes) query: ListNotes, @User() user: UserT): Promise { return this.notes.list(user.id, query.limit); } @Post("", { rateLimit: { max: 30, window: 60 } }) create(@Body(CreateNote) body: CreateNote, @User() user: UserT): Promise { return this.notes.create(user.id, body); } @Delete("/{id}") remove(@Param("id") id: string, @User() user: UserT): Promise { return this.notes.remove(user.id, id); } } ``` Every route needs a signed-in user unless it, its controller or the app's config says otherwise (`{ auth: false }`); `@User()` is that user. A body that fails its schema is a `400` naming the fields, a caller over the route's limit a `429`, a thrown `NotFound` a `404` — all in the platform's one envelope ([Responses & Errors](/docs/backend/errors)). A handler that returns nothing answers `204`; anything else is JSON. ### 5. The module ```ts // src/modules/notes/notes.module.ts import { Module } from "palbase/backend"; import { NotesController } from "./notes.controller"; import { NotesService } from "./notes.service"; @Module({ controllers: [NotesController], // its entry points providers: [NotesService], // the classes it owns exports: [], // which of them another module may reach imports: [], // whose exports this module may reach }) export class NotesModule {} ``` That list **is** the registration, and the only place ownership is decided. `exports` stays empty until another domain needs one of these classes; until a name appears there, nothing outside the module can reach it. Imports are not transitive: a module reaches the exports of the modules it imports, and of no others. ### 6. The root and the boot ```ts // src/app.module.ts import { Module } from "palbase/backend"; import { NotesModule } from "./modules/notes/notes.module"; @Module({ imports: [NotesModule] }) export class AppModule {} ``` ```ts // src/main.ts import { PalbaseFactory } from "palbase/backend"; import { AppModule } from "./app.module"; import schema from "./db/public"; // Serves on $PORT with its database; run by a release with PALBASE_DB_MIGRATE, // it migrates the database to this schema and exits. await PalbaseFactory.create(AppModule, { db: { schemas: [schema] } }); ``` At boot the container reads every module, refuses what is wrong — a class no module lists, a dependency from a module that is not imported or a class it does not export, a cycle — naming the path that led to it, builds every class once, runs their `onStart()` and serves. The project's address, id and secret key come from the machine's environment. ## Never write this These are the shapes a code assistant reaches for by default. Every one is wrong here: - **Express-style handlers** — `app.get(...)`, `(req, res) => …`. A route is a **method on a class**. - **A `ctx` object or middleware** — there is no context to thread and no pipeline to mount; auth, validation, limits and the error envelope are the framework's. - **A class no module lists** — refused at boot, by name. - **`@Inject`, `useFactory`, `forwardRef`** — a dependency is named by its constructor parameter's type and nothing else. - **An `interface` as a dependency** — it has no runtime existence. Use a class, or an `abstract class` with one listed subclass: the container gives the one owned subclass for it, and refuses by name when there are none or two. - **NestJS lifecycle names** — `onModuleInit`, `onApplicationBootstrap`, `onModuleDestroy` … are refused at boot. The hooks are `onStart()` and `onShutdown()`. - **A hand-wired singleton** — `export const notes = new NotesService()`. Mark it `@Injectable()`, list it, and let the container build it once. - **I/O in a constructor** — it runs while the app comes up, synchronously. Start-up work goes in `onStart()`, which is awaited before the first request. - **A timer for scheduled work** — a machine that sleeps runs no timer, and three copies run three. Use a [cron](/docs/backend/cron), or a [task](/docs/backend/tasks) for work done once. - **State in the instance that must outlive a request** — a machine may run several copies, and sleeps; what must be shared goes in the database. - **A migration file** — there are none. The schema file is the declaration. - **`try { … } catch { return 500 }`** — throw the error class and let the framework build the envelope; swallowing it turns a `404` into a `500`. - **Hand-built responses** — no `new Response(...)`, no `JSON.stringify`. Return the value; return nothing for `204`. - **A secret in the repository** — the secret key and every credential are project secrets the machine starts with; `.env.example` names them, never their values. ## Adding a feature 1. `src/db/public.ts` — declare the table and its policies. 2. `src/modules//dto.ts` — the schemas, each a value and a type. 3. `src/modules//.service.ts` — the logic; `@Injectable()`, its dependencies as constructor parameters. 4. `src/modules//.controller.ts` — the routes; the service in the constructor, never `Database`. 5. `src/modules//.module.ts` — list them; name the module in the root's `imports`. 6. Regenerate the tables' declaration file, if the schema changed ([Database](/docs/backend/database)). 7. Push. The release migrates the database beside the version that serves, then rolls the new version out. ## Give this to your agent For the full reference — every decorator, every module call, every schema builder — point your coding assistant at **`/llms.txt`**, the index of these docs, or **`/llms-backend.txt`**, the slice for writing a backend: getting started, the backend SDK, the server SDK and machines, in one file. Assistants fetch those when you name them; they do not go looking. ## Related - [Modules & Dependency Injection](/docs/backend/modules) — the four lists, and what is refused at boot - [Project Structure](/docs/getting-started/project-structure) — every file of a backend repository - [Controllers & Routing](/docs/backend/controllers) — the routing and decorator surface - [Request Validation](/docs/backend/validation) — `@Body`, `@QueryParams`, `@Param`, `@Headers` - [Responses & Errors](/docs/backend/errors) — the envelope, and the error classes - [Database](/docs/backend/database) · [Schema](/docs/backend/schema) · [Row-Level Security](/docs/backend/row-level-security) — the database half - [Testing](/docs/backend/testing) — services and the HTTP layer, with no network # Project Structure A Palbase backend is a directory a [machine](/docs/machines/overview) builds: a TypeScript app that Bun runs as it is, a `Dockerfile` that says how, and a `.env.example` that names what it must be given. Nothing in it is generated by a tool of Palbase's, and nothing is read by its location but the file `src/main.ts` starts with — what exists is what the modules list. This page walks that directory file by file. The layout is the one Palbase's own examples and Studio's backend use. ## The layout ```text notes-backend/ ← the machine's directory (its `path`) ├── Dockerfile how the machine builds and runs it ├── .env.example the variables it must be given ├── package.json its dependencies — `palbase` among them ├── bun.lock what `bun install --frozen-lockfile` installs ├── tsconfig.json decorators on, with their emitted types └── src/ ├── main.ts PalbaseFactory.create(AppModule, config) ├── app.module.ts the root module: imports every domain ├── db/ │ └── public.ts the `public` schema: tables, indexes, policies └── modules/ ├── notes/ │ ├── dto.ts schemas: each a value and a type │ ├── notes.controller.ts routes │ ├── notes.service.ts logic │ └── notes.module.ts what the domain owns, and exports └── core/ ├── core.module.ts └── health.controller.ts ``` Two rules decide everything else: - **A class exists when a module lists it.** A `@Controller`, an `@Injectable()` or an `@On` handler that no module lists stops the boot, naming it. A folder grants nothing. - **A domain is a folder with a module in it**, named in the root module's `imports` (or in the imports of a module the root imports). ## `src/main.ts` The process starts here, and it is short: ```ts // src/main.ts import { PalbaseFactory } from "palbase/backend"; import { AppModule } from "./app.module"; import schema from "./db/public"; await PalbaseFactory.create(AppModule, { db: { schemas: [schema] }, cors: { origins: ["https://app.example.com"] }, }); ``` `PalbaseFactory.create` builds every class, runs their `onStart()` and serves — on `$PORT`, which the machine sets. Everything in its config is the backend's own, with a default: | Option | Default | | |---|---|---| | `palbase` | from the environment | The project: `url` (`PALBASE_URL`), `projectId` (`PALBASE_PROJECT_ID`), `secretKey` (`PALBASE_SECRET_KEY`), `webhookSecret` (`PALBASE_WEBHOOK_SECRET`) | | `auth` | a signed-in user | Every route's auth unless the route or its controller says otherwise | | `rateLimit` | 300 a minute per caller | Every route's limit unless it says otherwise; `false` for none | | `bodyLimit` | 1 MiB | The largest request body | | `cors` | off | The origins browsers may call from: `{ origins: [...] }` or `"*"` | | `db` | none | The app's database: its `schemas`, and its address unless `PALBASE_DB_URL` says | | `openapi` | `true` | Serve the app's contract to the platform, which Studio's backend screens read | | `port`, `host` | `$PORT` (else 8080), `0.0.0.0` | Where it listens | | `boot` | 5 attempts, 1 s doubling | A start whose `onStart()` fails is tried again from scratch | A value missing or malformed stops the boot before any request is served, naming both ways to give it. What cannot heal by waiting — a missing setting, a DI mistake — stops at once; a database not up yet is waited for. See [Backend SDK](/docs/backend/overview). ## `src/app.module.ts` ```ts // src/app.module.ts import { Module } from "palbase/backend"; import { CoreModule } from "./modules/core/core.module"; // One import per domain: NotesModule joins this list beside CoreModule. @Module({ imports: [CoreModule] }) export class AppModule {} ``` The root owns nothing of its own here; it is the list of domains. A small app may list a controller or two in it directly. ## `src/modules//` One folder per domain, and in it the four files of [Architecture](/docs/getting-started/architecture): the module, the controller, the service and the `dto.ts`. A domain that needs another's class names it in a constructor, and its module imports the module that exports it: ```ts // src/modules/core/health.controller.ts import { Controller, Get } from "palbase/backend"; @Controller("/health", { auth: false }) export class HealthController { @Get("") health(): { ok: true } { return { ok: true }; } } ``` ```ts // src/modules/core/core.module.ts import { Module } from "palbase/backend"; import { HealthController } from "./health.controller"; @Module({ controllers: [HealthController] }) export class CoreModule {} ``` A route's path is its own: `/health` above is the app's, at the machine's address. Two first segments are not yours — `webhooks`, where the platform delivers the app's events and each `@Webhook` class takes a third party's ([Webhooks](/docs/backend/webhooks)), and `_palbase`, the platform's own way in — and a controller mounted under either is refused. ## `src/db/` One file per Postgres schema, each with a **default export**, `defineSchema("", { tables })`: `public.ts` for `public`, `billing.ts` for a `billing` schema. `src/main.ts` hands them to `db.schemas`. ```ts // src/db/public.ts import { defineSchema, defineTable, text, timestamp, uuid } from "palbase/backend"; const notes = defineTable("notes", { columns: { id: uuid().primaryKey().defaultRandom(), owner: text(), title: text(), created_at: timestamp().defaultNow(), }, }); export default defineSchema("public", { tables: [notes] }); ``` A release migrates the database to them beside the version that serves; there are no migration files ([Schema](/docs/backend/schema), [Schema changes](/docs/backend/migrations)). `Database.public.` is typed by a declaration file written from those schemas with Palgres' `makeEnvDts` — commit it beside them, and write it again when a schema changes ([Database](/docs/backend/database)). An app with no database has no `src/db/`, and no `db` in its config. ## `package.json` ```json { "name": "notes-backend", "private": true, "type": "module", "scripts": { "start": "bun run src/main.ts" }, "dependencies": { "palbase": "^0.10.0" } } ``` The SDK is one npm package, `palbase`: `npm install palbase` (or `bun add palbase`). A backend imports the framework from `palbase/backend`, which brings what it is built on — `palbase/server` (the project's modules, as `Palbase`) and `palgres` (the database) — and `reflect-metadata`, which the framework loads itself. The package ships compiled JavaScript with its type declarations; Bun runs the app's own TypeScript as it is. `z` from `palbase/backend` is the zod the framework is built with: import it from there, and the app and the framework agree on one. `palbase init` writes this `package.json`. ## `tsconfig.json` ```json { "compilerOptions": { "target": "es2023", "module": "esnext", "moduleResolution": "bundler", "strict": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "allowImportingTsExtensions": true, "noEmit": true, "skipLibCheck": true }, "include": ["src/**/*.ts"] } ``` `experimentalDecorators` and `emitDecoratorMetadata` are what make constructor injection work: the container reads each constructor's parameter **types** from the metadata TypeScript emits. Bun reads them from this file. A runner that does not emit the metadata — esbuild, and the tools built on it — leaves the container nothing to read, and the boot stops naming the class and the cure. `noEmit` is right: nothing is compiled ahead of time, and `tsc --noEmit` is the type check. ## `Dockerfile` ```dockerfile FROM oven/bun:1.4.2-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f WORKDIR /app COPY package.json bun.lock ./ RUN bun install --frozen-lockfile --production COPY tsconfig.json ./ COPY src ./src USER bun CMD ["bun", "run", "src/main.ts"] ``` The machine builds the directory's `Dockerfile` when it has one; without one, Railpack detects what the directory is and plans the image ([Builds](/docs/machines/builds)). A backend ships its own, for two reasons: the decorators need `tsconfig.json` beside the sources, and `bun run src/main.ts` is exactly how the app is meant to start. Pin the base image by digest, so a rebuild of the same commit builds the same image. The process listens on `$PORT` and stops cleanly on `SIGTERM`: it finishes the requests in flight (20 seconds at most), runs `onShutdown()`, and exits — which is what a rollout asks of the version it replaces. ## `.env.example` ```text # The names this backend reads. The machine is started with them; # their values are never in the repository. PALBASE_URL= PALBASE_PROJECT_ID= PALBASE_SECRET_KEY= ``` A build reads it as the list of variables the program needs, and does not roll out until the machine has every one — its own environment, its secret set, or what the platform gives it. It names variables, never their values: commit it, and keep the values in the project's secrets ([Builds](/docs/machines/builds)). ## What the process is given On a machine, the program finds these in its environment: | Variable | Set by | | |---|---|---| | `PORT` | the platform | The port to listen on | | `PALBASE_URL` | the platform | `https://.palbase.studio` | | `PALBASE_PROJECT_ID` | the platform | The project's id — the `project_id` its access tokens carry | | `PALBASE_REPLICAS` | the platform | How many copies of the machine run | | `PALBASE_PLATFORM_SECRET` | the platform | The key the platform signs its calls to the machine with | | `PALBASE_DB_URL`, `PALBASE_DB_CONNECTIONS` | the platform, given a database | Its address as the app login, and one copy's share of its connections | | `PALBASE_SECRET_KEY` | the platform | The machine's own secret key of the project (`machine: ` among its API keys): `Palbase.*` calls, and checking callers' sessions | | `PALBASE_WEBHOOK_SECRET` | the platform | The secret the project's events reach the machine signed with, at the endpoint the platform keeps for it | And what it answers the platform, at paths the internet never reaches: `GET /_palbase/health` — `200` while it serves, `503` while it stops, the machine's health check. Its contract is written by its build (run with `PALBASE_CONTRACT_OUT`, the app writes it and exits), not asked of it while it serves. ## Running it on your own machine The same directory runs anywhere Bun does: ```bash PALBASE_URL=https://.palbase.studio \ PALBASE_PROJECT_ID= \ PALBASE_SECRET_KEY=pb__s… \ PORT=8080 bun run src/main.ts ``` It is a client of the project like any other: the secret key reaches the project's modules, and a caller's token is checked against the project. Studio does not show the project's id, which a machine is given; it is the `project_id` claim of any access token the project issues — the one your web app's session holds. Off a machine there is no `PALBASE_PLATFORM_SECRET`, so the contract is not served. An app with a database also needs `PALBASE_DB_URL`, a database whose schema it can bind to; a release migrates the database before the version that reads it starts — locally, run the app once with `PALBASE_DB_MIGRATE=start` and the owner's address in `PALBASE_DB_OWNER_URL` ([Schema changes](/docs/backend/migrations)). ## In a repository of several apps A backend need not be a repository of its own. In a monorepo it is one directory of it — the machine's `path` — and an app that imports a sibling package is built with the repository's root as its `context`, its `Dockerfile` copying from there ([Connecting GitHub](/docs/machines/github#one-repository-several-machines)). A push builds the machines whose directory it changed, and no other. ## Related - [Architecture](/docs/getting-started/architecture) — what goes in each file, and why - [Quickstart](/docs/getting-started/quickstart) — this layout, end to end - [Modules & Dependency Injection](/docs/backend/modules) — the four lists, and what the boot refuses - [Builds, Logs & Rollback](/docs/machines/builds) — what the Dockerfile and `.env.example` decide - [Machines](/docs/machines/overview) — the environment, the health check, and the port # Access Tokens An **access token** lets a CI job or an agent act on an organization without a person signing in. It is set as the `PALBASE_TOKEN` secret of the job, and works the way a fine-grained personal token on GitHub or an access token on Vercel does, at the level of the organization. Admins and owners manage tokens in Studio, under **Organization › Access tokens** (`/organizations/{id}/tokens`). Open the organization switcher at the top left and choose **Access tokens**, or choose it in an organization's left menu, or from **Access** in any of its environments. Members do not see them. ## Creating one **Create token** asks for: | Field | Meaning | |---|---| | Name | What the token is for, up to 100 characters on one line: `GitHub Actions` | | Role | **Member** or **Admin**. Never owner | | Expires after | 7, 30, 90 (the default), 180 or 365 days, or a number of days from 1 to 365 | The token starts with `pbo_`. Studio shows it **once**, when it is created. Store it as your CI's `PALBASE_TOKEN` secret; you will not see it again. Afterwards the list shows only its last four characters, as `pbo_…a1b2`, with who made it, when it was made, when it expires and when it was last used. If the answer to creating one is lost, Studio does not send the request again: it asks you to check the list. A token that was made but whose value was lost is deleted and made again. ## What a token can do - It acts on the organization's projects and environments with its role: member or admin, never owner. - An admin token opens projects and environments, registers a project's apps and sets them in each environment, and adds an environment's webhook endpoints — what a program that opens a project for each product does with no one signed in, as a Firebase service account adds an app. - An admin token can get a CLI key. That key lives at most one day and appears in the environment's API keys as `CLI — token ()`. - An admin token links a GitHub installation that another environment of the organization linked to an environment, with no GitHub sign-in: `POST /v1/environments/{ref}/github/installations` with `{ "installation_id": 123, "from": "" }`. The environment reaches the repositories the GitHub user who linked it there can push to, and repositories created later that they can push to. With `"repositories": ["owner/name", …]` (the CLI's `github:connect --from --repo owner/name`) it reaches those alone, and never one created later: what a program that opens a project for each product gives each product, so that one product's environment builds nothing of another's. - An admin token mints [environment tokens](#environment-tokens). ## What a token cannot do - It cannot do what only a person can: manage the organization, its members, roles and invitations, or tokens; create or revoke an environment's API keys; or reach Palbase's own environment. - It never has more than its creator's **current** role. If the creator becomes a member, their admin tokens stop working at once. If the creator leaves the organization or is deleted, their tokens go with them. ## Deleting one Deleting a token makes its next request fail, and the CLI keys it got are revoked. Deleting does not undo what the token did: the deploys it made and the settings it changed stay. The webhook endpoints a token made stay too, and keep receiving events: an endpoint's secret signs what is delivered to it and opens nothing of the platform, so it belongs to its environment, as with Stripe's and GitHub's webhooks. Each endpoint names who made it in **Settings › Webhooks**, and when you delete a token Studio lists the endpoints it made, by address and environment, before you confirm (`GET /v1/orgs/{id}/tokens/{token}/webhooks`). Remove them in their environment if you no longer want them. API keys, which do open the platform, are made by a person only. An expired token is marked **Expired** in the list and no longer works; delete it to tidy the list. ## Environment tokens An **environment token** is for a program that works in **one** environment — an AI agent building one product — for a few hours, the way a GitHub App's installation token is for one installation. It starts with `pbe_`, and it is set as that program's `PALBASE_TOKEN`. It is minted with `POST /v1/environments/{ref}/tokens`, by an admin of the organization (signed in, or with an admin access token) or with the environment's own **secret key** in the `apikey` header and no one signed in: ```json { "expires_in_minutes": 60, "name": "agent" } ``` Studio shows it **once**. It lives 60 minutes unless you ask otherwise, 240 at most, and never longer than the access token or key that minted it. ### What it can do In its environment it does what the role of whoever minted it does there, as their own CLI would: an admin's token reads and changes the environment's machines, databases, users, flags, apps and webhooks, and sets its secrets. A token minted with the secret key acts as an admin. The role is read on every request, so a token never holds more than its minter holds now. Its changes appear in the organization's audit log under its name. Outside its environment it does nothing. Another environment of the project answers `404`. The project itself and the organization answer `403 token_out_of_scope`, and so does anything that changes the project (its name, its apps, its environments). When it reads its project, it sees only its own environment. **A full token of the admin role, or one minted with the secret key, is, in what it can reach, an admin of its environment.** It deploys machines, and the code it deploys runs with a secret key of the environment (`PALBASE_SECRET_KEY`, which the platform gives every machine) and reads the environment's secrets. What that code takes, it keeps after the token has ended. So: - give a full token only to an agent you would trust as an admin of that environment; - for narrow work, mint a token with [`scopes`](#narrowing-it): no scope deploys anything; - the closed list below closes the routes that make or show a credential in their own answer, not what deployed code can do. ### What no environment token can do These routes are closed to every environment token: each one makes an identity, or makes or shows a credential that would outlive the token, in its own answer. Everything else is open at the minter's role, deploying code included (see above). They answer `403 token_out_of_scope` (or `person_only`), whoever minted the token, in Studio and at the environment's modules alike: - mint a token: an environment token, or an organization's access token; - create, revoke or list API keys, or take a CLI key; - change the organization's members, roles or invitations; - delete the environment or the project; - link a GitHub installation from another environment; - sign in as one of the environment's users, or make a password reset for one; - set a user's password or email: create or import users, change a user's email (test users are its to make); - create an OAuth client or a service account, rotate one's key, or change where an OAuth client's codes go; - define a role, or give or take one from a user; make, change or remove an auth hook, whose claims go into every user's token, an app's role among them; - create a database, or make new passwords for one: both answers show its passwords; - read a secret's value, a task's payload (it may carry a token) or a coupon's one-time codes. Setting a secret is allowed; - set or remove a sign-in provider's client secret, or change which apps' sign-ins with it the environment takes (its client ids); - bring or remove a message provider (where the project's mail and texts are sent, and with whose credentials); - override or remove a template that carries a sign-in or reset credential — a reset or magic-sign-in link, a verification or invitation link, a one-time code — on email, SMS or WhatsApp. The app's own notification templates stay open, so an agent still brands them. The last three close the roundabout ways to a user's own credential: a sign-in provider's client of one's own, a message provider aimed at a server of one's own, or a reset template that tucks the link into one it fetches, would catch a sign-in the way signing in as the user would. The sender's own settings stay open — a token sets the project's `from` address and reply-to (`/notify/settings`), and the site and redirect URLs a new product signs in against (`/admin/auth-settings`). These change where a link points, but no credential leaves unless the user follows the link themselves. Each module refuses these itself, on the route it matched, so writing the path another way does not get past this list: an escape, a dot segment, other letter case or a slash at the end all name the same thing. A link the token signs does not outlive it either. A signed download or upload URL, a package's upload link, a link to hand over an Apple or Stripe key, a signed bundle's download link and the link that connects GitHub all end no later than the token. The answer's `expires_at` says when. Test users, and the sign-ins made for them, are open: they are test fixtures, and `auth:test-users` is for them. ### Narrowing it `scopes` narrows a token to what they name, and nothing else is reached, a read as much as a change: ```json { "scopes": ["machines:read", "machines:logs", "auth:test-users"], "expires_in_minutes": 60, "name": "agent" } ``` | Scope | What it reaches | |---|---| | `machines:read` | The environment's machines, their builds, crons and queues (not a task's payload), contracts and metrics | | `machines:logs` | A machine's logs and its builds' logs | | `apps:sync` | What `palbase apps:sync` reads: the project's apps as they are in this environment, their client settings, the machines' contracts | | `auth:test-users` | The environment's test users: made with their sign-ins, listed, deleted. Not other users | | `support` | A question to Palbase's support line (`palbase support:ask`, `POST /ai/support-questions`): an agent asks Palbase what it cannot find out, not the person it works for | ### When it stops working It stops working when it expires, when it is revoked (`DELETE /v1/environments/{ref}/tokens/{id}`, by an admin, by a token of an admin's role, or by a narrowed token itself), and when what minted it no longer holds: the admin is no longer an admin, the access token is deleted, or the secret key is revoked (within a minute). `GET /v1/environments/{ref}/tokens` lists the live ones by their last four characters. # Billing Each environment is billed on its own, by the calendar month (UTC): it has its own plan, its own credit and its own free quota, as a project of its own. The organization holds only what pays — its cards, its billing details and its bank transfers — and an environment's credit is never another's unless an owner or an admin moves it. Each environment is on one of two plans: - **Free**: charged nothing. Every item has a free quota each month, and past it the environment is slowed or stopped ([Usage & Limits](/docs/getting-started/usage-and-limits#what-a-free-project-gets)); its machines and databases are of the free sizes. Every environment starts on Free. Until one of the organization's environments has paid a top-up, a person opens at most 2 projects of 2 environments each — one deleted counts until the month is out, so deleting and opening again brings no new month of free quotas — and a Free project is opened by a person, not an organization's access token. - **Pay as you go**: the environment has credit, loaded ahead in US dollars; its first top-up by card moves it there. What it uses past its own free quota is priced and taken from its credit as it is used, every few minutes; it may open every size of machine and database. There is no monthly fee and no bill afterwards. When an environment's credit runs out, or its month reaches its limit, only it falls back to the free quotas; the organization's other environments go on. The prices, the free quotas and the terms are on the [billing terms](https://palbase.studio/legal/billing) page. ## Opening the payment account An owner or admin opens it in Studio, on the organization's **Billing** page, by accepting the terms for the organization (Studio records who, when, and which version). Nothing is charged. From then on each environment is moved to Pay as you go by its first top-up by card, of $10 or more: **Billing › Environments › Upgrade** on the environment, paid on Stripe's page the first time, which asks for the card, the billing address and, for a business, its VAT number, and saves the card for the organization. Tax is added on top as the billing address requires. Stripe emails an invoice for every top-up, naming its environment; **Billing › Invoices** lists them. Once one of its environments has paid a top-up, the organization is not held to the Free limits on projects and environments. ## Top-ups Each environment is topped up on its own, from **Billing › Environments › Manage**: - **Now**: **Top up** — a usual amount or any other, from any of the organization's saved cards, or another card on Stripe's page (it is saved for next time). - **Automatic**: when the environment's credit comes under a threshold, it is topped up by an amount, at most once an hour; on by default ($20 when under $5), and never past what the environment's monthly limit still needs. **Automatic top-up** changes both or turns it off. It is taken from the organization's default card; when that card declines, the same invoice is paid from the organization's other cards in turn — it is paid once at most. When every card declines, it is tried again once a day for two days, the owners and admins are emailed each time, and while a card declined today no other environment's automatic top-up tries it again that day. A declined invoice is voided (**Invoices** shows it as declined), and each try is a new one. A refund or a dispute of an environment's top-up turns its automatic top-up off. - **By bank transfer**: **Top up › Bank transfer** makes an invoice with bank details of the organization's own; the environment gets the amount when the transfer arrives — one on Free is then moved to Pay as you go with **Manage › Move to Pay as you go**. What the organization's transfers paid past their invoices stays with it, and pays the next top-up of any of its environments first, when it covers the whole of it — a card is charged only otherwise; what is left of it 75 days after it arrived, Stripe returns to the account it came from. An organization with no card is topped up automatically from what its transfers left, while it covers the top-up. Credit can be spent for 12 months; what is left of it then expires. The environment's sheet lists each top-up, bonus and credit moved in with what is left of it and when it expires; the one closest to expiring is spent first. ## Moving credit between environments An owner or an admin moves an environment's credit to another environment of the same organization: all of it, or an amount (**Manage › Move credit**). Each part keeps its expiry. Credit never leaves the organization, and a member cannot move it. An environment whose credit is moved away entirely is moved to Free. Deleting an environment, or a project and its environments, offers to move their credit to another environment first. A deleted environment keeps its credit until it is purged, 7 days after it was deleted — **Billing › Environments** offers to move it until then — and what is left of it is lost when it is purged. The organization's owners and admins are emailed when an environment is deleted holding credit, with how much and when it is lost. ## Cards and billing details **Billing › Payment** lists the organization's cards, shared by its environments. **Add card** saves one on Stripe's page — nothing is charged — and makes it the default unless you say otherwise; any other card can be made the default or removed. The default card is removed only when it is the last one; the automatic top-ups then wait for a card, or a bank transfer. **Edit billing details** sets the name, address and tax number (a VAT number in the EU) the next invoices are made out to. The address is checked for tax at once: one that cannot be placed is refused, and nothing changes. ## What is charged **Usage** shows the month: each environment's plan and what it used, and to an owner or an admin what that came to before its free quota (gross) and what it was charged past it. Any member reads what was used; the amounts are an owner's or an admin's. **Download CSV** gives it item by item — a line per environment and item, units, gross and charged, with its total and what its credit gave — for an organization that bills its own customers per environment. Palbase AI's models, on Palbase's own provider keys, are their provider's price and 10%, and each month brings each environment $1.00 of them free. A project may have a monthly AI budget of its own ([AI](/docs/backend/ai#what-it-costs)). A call under the project's own provider key is the provider's to bill. Requests, push notifications, and email, SMS and WhatsApp sent through the project's own provider are not charged. ## Each environment's own For each environment, an owner or an admin can, on **Billing › Environments › Manage** or from the API: - set a **monthly limit**: at it, the environment falls back to the free quotas until the month ends, while the others go on; - move it to **Free**: it is charged nothing and held to the free quotas; its credit stays, spent once it is back on Pay as you go; - **freeze** it: its requests are refused (`403 project_frozen`), its machines and databases stop, its data is kept, and only what it stores is charged. Unfreezing brings it back. ## When an environment's credit runs out Nothing is owed. What it pays for stops: its machines and databases of a paid size stop (`402 machine_not_paid` while it lasts), Palbase's AI models past its month's free dollar are refused (`402 ai_payment_required`; a project's own provider key goes on), and it is held to its free quotas, as on Free; what is within them goes on. Usage measured in the few minutes before it stopped is taken from its next top-up; what it uses after it is not charged. The organization's other environments are not touched. A top-up brings it back by itself. ## Emails and webhooks The organization's owners and admins are emailed when an environment's credit comes under the days of spend set for it (3 by default, in **Automatic top-up**) — at most once a day —, when it runs out, when its automatic top-up is declined, and when its month reaches its limit — and when an environment is deleted holding credit, a few minutes after, unless it was moved or the environment restored meanwhile. What happens at once to several environments comes in one email, naming each. Each environment's own webhooks are told the same as it happens, as `billing.low_balance`, `billing.balance_exhausted`, `billing.topup_failed` and `billing.limit_reached` ([Webhooks](/docs/server/webhooks#the-events)) — an organization that runs its customers' apps tells them from there. They carry no amount: the amounts are the organization's owners' and admins', on its bill. ## From the terminal The CLI does what the Billing page does, as you or with an admin's [access token](/docs/getting-started/access-tokens) in CI — save opening the payment account and paying on Stripe's page, which are a person's: ```bash palbase billing # each environment's plan, credit, month and limit palbase billing:env shop/staging # an environment's own: its plan, credit, limit and automatic top-up palbase billing:env shop/staging --limit 25 --auto-topup yes --threshold 10 --amount 50 palbase billing:env shop/staging --plan free # or pay_as_you_go; --frozen yes|no palbase billing:topup shop/staging 20 # from the saved card, $10 to $10,000; a Free one moves to Pay as you go palbase billing:bank-transfer shop/staging 500 # an invoice to pay by bank transfer palbase billing:transfer shop/staging shop/main 5 # credit moved, with its expiry; all of it when no amount is named palbase billing:ledger shop/staging --limit 50 # every change of its credit, newest first palbase billing:invoices # the top-ups' invoices, each with its environment palbase billing:charges --period 2026-10 --csv # the month's charges by environment and item, as the page downloads them ``` The environment is `project/environment` or its ref, else the one chosen for the directory; the organization is `--org`'s, else that environment's. Amounts are in US dollars, to the cent. A member reads the plans; the money is an owner's or an admin's. ## The API What Studio's Billing page does, an admin's [access token](/docs/getting-started/access-tokens) does too — save opening the payment account and paying on Stripe's page, which are a person's. The calls are on Studio's API, under the organization: | Call | What it does | |---|---| | `GET /v1/orgs/{id}/billing` | The payment account, and each environment's plan, credit, month and limit | | `GET /v1/orgs/{id}/billing/environments/{ref}` | An environment's own: its plan, credit, automatic top-up, limit and month | | `PATCH /v1/orgs/{id}/billing/environments/{ref}` | Its `plan` (`free`, `pay_as_you_go`), `frozen`, `monthly_limit_micros` (`null`: none), `auto_topup`, `auto_threshold_micros`, `auto_amount_micros`, `low_balance_days` | | `POST /v1/orgs/{id}/billing/environments/{ref}/topups` | A top-up of it from what bank transfers left, else the default card or the one named: `{ "amount_micros": 20000000, "payment_method": "pm_…" }`; `"plan": "pay_as_you_go"` moves a Free one with it | | `POST /v1/orgs/{id}/billing/environments/{ref}/bank-transfers` | An invoice to pay by bank transfer, for it | | `GET /v1/orgs/{id}/billing/environments/{ref}/ledger` | Every change of its credit; `?kind=topup,bonus,transfer` for the credits alone | | `POST /v1/orgs/{id}/billing/transfers` | Credit moved: `{ "from": "", "to": "", "amount_micros": 5000000 }`, all of it without `amount_micros` | | `DELETE /v1/projects/{id}?move_credit_to=` | Deletes a project, its environments' credit moved first; `DELETE /v1/environments/{ref}?move_credit_to=` an environment | | `GET /v1/orgs/{id}/billing/cards` | The cards, the default first | | `PUT /v1/orgs/{id}/billing/cards/{card}/default` | Makes a card the default | | `DELETE /v1/orgs/{id}/billing/cards/{card}` | Removes a card; the default only when it is the last | | `GET` / `PUT /v1/orgs/{id}/billing/customer` | The name, `address` and `tax_id` (`{ "type": "eu_vat", "value": "…" }`, or `null`) the invoices are made out to | | `GET /v1/orgs/{id}/billing/charges?period=2026-10` | The month's charges, by environment and item: units, gross and charged | | `GET /v1/orgs/{id}/billing/charges.csv?period=2026-10` | The same, as CSV | | `GET /v1/orgs/{id}/billing/invoices` | The top-ups' invoices, each with its environment; `?environment=` one's alone | Amounts are in micro-dollars (1,000,000 is $1), in whole cents. A member reads the plans and what was used; amounts and money are an admin's. ## Related - [Usage & Limits](/docs/getting-started/usage-and-limits) — what is metered, and what a free environment gets - [Organizations](/docs/getting-started/organizations) — roles, and who is emailed - [Access Tokens](/docs/getting-started/access-tokens) — an admin token for the API # Usage & Limits Every environment's use is metered by the calendar month (UTC): what its modules did, what its machines and databases ran, and what it sent. Studio shows it on the project's **Settings › Usage**, environment by environment. An environment is held to limits of three kinds, and each one says so in its answer: - **Rates**, per second: a call over one is refused for that second (`429` with `Retry-After: 1`). - **Limits on the month**: a limit set on one of the environment's metrics refuses that use, or only counts it, until the month ends. - **What a free project gets**: its traffic is slowed past 5 GB, its chat messages past a million a month or 1 GiB kept, and a free database sleeps past its 400 hours. None of them stops the project or loses its data. ## Where usage is shown **Project › Settings › Usage** shows one environment at a time (choose it at the top when the project has more than one) over the last 3, 6 or 12 months: - this month's API requests, monthly active users, stored data and machine hours, each beside its months; - **Limits**: each limit set on the environment, with what this month has used against it, and **Blocked** once a limit that refuses is reached; - a chart per month of requests, active users, notifications sent by channel, stored data, download traffic, realtime messages, analytics events, machine hours, machine internet traffic, response traffic and database hours by size; - **Every metered item**: every metric the environment used, this month and the month before. Every member of the organization reads it ([Organizations](/docs/getting-started/organizations#roles)). The organization's owners are also mailed when a free project has sent 80% of its month's traffic and when it has sent all of it, and when a free database has been awake 80% of its hours and all of them. ## What is metered | Metric | What counts | |---|---| | `requests` | **API requests**: each request to the environment's address that a module answers. Not among them: the cache's calls, the stores' reports to purchases (`/purchases/webhooks/…`), and the files the platform moves for a module's own work, such as an AI workspace's | | `auth.mau` | **Monthly active users**: the users who signed in or kept a session alive in the month | | `notify.email`, `notify.push`, `notify.sms`, `notify.whatsapp` | **Email**, **Push**, **SMS** and **WhatsApp** sent: an email through any sender (those through Palbase's shared sender are also `notify.email.shared`), a push delivered to a user on any of their devices, a text or a verification code, a WhatsApp message | | `storage.bytes` | **Stored data**: the objects and their renditions, the month's largest | | `storage.egress` | **Download traffic**: what storage sent, a part of response traffic | | `storage.operations` | The requests storage served | | `storage.transforms` | **Image transformations**: a picture made over from an original (resized, cropped, re-encoded); one answered from what was made before is not one | | `cache.bytes`, `cache.egress` | **Cache memory**, the month's largest, and **Cache reads**, a part of response traffic ([Cache](/docs/backend/cache)) | | `realtime.messages` | **Realtime messages**: each one delivered — a broadcast to ten subscribers is ten | | `realtime.connections` | Realtime connections open at once, the month's largest | | `messaging.messages` | **Chat messages**: each message the project's people, or its server, sent in its conversations; one sent again with the same client id is one | | `messaging.bytes` | **Chat message storage**: what the project's messages weigh where they are kept — each one's sealed text and 768 bytes for its row and indexes — the month's largest | | `flags.reads` | The flag reads clients made, answered in full or not modified | | `analytics.events` | **Analytics events**: captures, identifies, aliases, screens and page views (setting a person's properties is not counted) | | `analytics.perf`, `analytics.traces`, `analytics.console` | Performance samples kept, request traces taken, console records kept ([Analytics](/docs/analytics/overview)) | | `machines.seconds` | **Machine hours**: how long the machines were awake, each copy counted; `machines.seconds.` the same by size | | `machines.egress` | **Machine internet traffic**: what the machines sent to the internet | | `edge.egress` | **Response traffic**: what the environment's answers sent its clients, its modules' and its machines' alike | | `machines.builds.seconds` | **Build hours** | | `machines.images.bytes`, `machines.logs.bytes` | **Machine images** kept, the month's largest, and **Machine logs** kept | | `databases.seconds.` | **Database hours**: how long the databases of each size were awake | | `databases.bytes`, `databases.backups` | What the databases hold, and what their backups hold, the month's largest | | `databases.traffic` | What the databases read from and wrote to their disks | | `databases.egress` | **Database client traffic**: what the databases sent clients that connect to them over their own addresses (`psql`, a backend on your computer) | | `ai.tokens...` | **AI tokens** with Palbase's provider keys, by model and kind: `input`, `image_input`, `output`, `cache_read`, `cache_write`. A model used with the project's own provider key is the provider's to count | | `ai.images..` | **AI images** made, by model | | `ai.machines.seconds`, `ai.voice.seconds`, `ai.web_search.calls` | **AI workspace hours**, **AI voice hours** and **AI web searches** | A metric that is "the month's largest" is a level, not a sum: what the environment held at its most during the month. ## Rates | What | How much | Past it | |---|---|---| | The environment's requests | 200 a second, unless Palbase set another | `429 rate_limited`, `Retry-After: 1` | | What apps send to analytics | 1,000 events, samples, traces and records a second, counted apart from the requests | `429 rate_limited` | | The cache's calls | As many a second as the requests, counted apart from them; 8 MiB read and 2 MiB written a second | `429 rate_limited` | | Your backend's own routes | What each route's `rateLimit` says ([Controllers & Routing](/docs/backend/controllers)) | `429 too_many_requests` | Realtime has rates of its own, which a project may lower ([Web Realtime](/docs/web/realtime)), and so do sign-ins and the messages a project sends. The SDKs send a refused call again on their own, waiting what `Retry-After` asks, when it is a read or a write that cannot be done twice ([Server SDK](/docs/server/overview#retries)). ## Limits on the month A limit on the month names one metric of one environment and how much of it the month may use, and is one of two kinds: one that **refuses** past it, and one that only **counts**. Palbase sets them; the **Limits** panel lists those set on the environment. A month's use starts again at 00:00 UTC on its first day. When a limit that refuses is reached, that use is refused until the month ends, and the refusal's `Retry-After` is the seconds until then: | Limit on | What is refused | |---|---| | API requests | Every request to the environment's address but those not counted (above): `429 quota_exceeded` | | Monthly active users | New sign-ups: `403 signups_closed`. Sign-ins go on: the people already counted keep their accounts | | Email, push, SMS or WhatsApp | Sending on that channel: `429 quota_exceeded` | | Stored data | Writing to storage: `429 quota_exceeded`. Reads go on | | Realtime messages or connections | A new socket: `429 quota_exceeded`, with `Retry-After: 3600` (an hour, not the month's end); and a server's broadcast: `429 quota_exceeded` | | Flag reads | Reading the flags: `429 quota_exceeded` | | Chat messages, or what they weigh | Sending a message, a person's or the server's: `429 quota_exceeded` | | Analytics events, samples, traces or console records | Sending that kind: `429 quota_exceeded` | | Machine hours | The machines: they are stopped, dedicated sizes too, and a request to one, a new machine, a cron or a task is `429 quota_exceeded` | A limit that counts refuses nothing: the use goes on and is metered. ## What a free project gets An environment is free while it is on the Free plan — every environment starts on it — and while its credit has run out or its month is at its monthly limit ([Billing](/docs/getting-started/billing)). Each environment is free on its own: one running out holds no other. - **Sizes.** Its machines are of the `shared-256` size, the one free machine size, and it has one database at most, of the `free` size: a project holds one database of a free size at a time ([Databases](/docs/machines/databases#sizes)). Every other size is paid for: opening a machine or a database of one, or moving one to it, is `403 size_not_available`. `palbase machines:sizes` and `palbase databases:sizes` list the sizes and which of them the environment may open now ([Machines](/docs/machines/overview#sizes)). - **Traffic.** It sends 5 GB a month at full speed: its machines' internet traffic and its response traffic together. Past it, until the month ends, its machines' traffic and answers and the files its storage hands out go at about 256 kbit/s; nothing is stopped, and the API's other answers are not slowed. A slowed project's downloads are paced, and one more than it may have under way at once is `429 rate_limited` with `Retry-After`. See [Reaching the internet](/docs/machines/overview#reaching-the-internet). - **Chat messages.** 1 million messages a month, and 1 GiB of them kept. Past either, until the month ends, the project's messages go at about one a second, all its people and its server together: a send waits its turn, 10 seconds at the most, and one whose turn is further off is `429 slowed` with `Retry-After` — the SDKs keep it in their outbox and send it again. No message is lost, and reading, syncing and everything else go on at full speed. What is kept counts at its month's largest, so a project that keeps more than 1 GiB is slowed again early each month: remove old messages, or set the project's retention ([Messaging](/docs/messaging/overview#the-projects-settings)), to stay under it. - **A free database** is awake 400 hours a month at most. Past them it sleeps until the month ends, refusing connections, with its data kept; from then its next connection wakes it. - **AI.** $1.00 of Palbase's models a month, at the provider's price and 10%. Past it, until the month ends, a call to them is `402 ai_payment_required`; a call under the project's own provider key goes on ([AI](/docs/backend/ai#what-it-costs)). An environment on Pay as you go with credit is never slowed for its traffic, and its machines and databases may be of any size; a database of the `free` size keeps its 400 hours whoever holds it. What it uses past its own free quotas is charged from its own credit ([Billing](/docs/getting-started/billing)). ## Each module's own limits | Module | Limits | |---|---| | Storage | 5 GiB an object unless the bucket says less; a signed URL lives 7 days at most ([Storage](/docs/backend/storage)) | | Cache | 25 MiB of entries, the ones nearest their end dropped to make room; 512 KiB a value ([Cache](/docs/backend/cache)) | | Tasks | 100,000 tasks not yet done, or 256 MiB of what they carry ([Tasks](/docs/backend/tasks)) | | Machines | 5 machines, 1 to 5 copies each, 50 builds a day, logs kept 7 days ([Machines](/docs/machines/overview#limits)) | | Databases | 3 a project, one of them of a free size at a time; a database full to its size's storage turns read-only ([Databases](/docs/machines/databases#sizes)) | | Analytics | Events kept 90 days unless changed, 365 at most ([Analytics](/docs/analytics/overview#how-long-it-is-kept)) | ## Related - [Billing](/docs/getting-started/billing) — each environment's plan and credit, and what is charged - [Logs](/docs/getting-started/logs) — every request, with its status - [Organizations](/docs/getting-started/organizations) — who sees the usage page, and who is mailed - [Machines](/docs/machines/overview) — sizes, sleep, and traffic to the internet - [Databases](/docs/machines/databases) — sizes, sleep and storage # Logs Studio's **Logs** page lists what happened in an environment, newest first, one category at a time: | Category | A line is | Its source | Its level | |---|---|---|---| | **Requests** | A request to the environment's address, whichever module it went to, as the platform's edge answered it — refused ones too | The module: `auth`, `storage`, `realtime`, `flags`, `notify`, `analytics`, `secrets`, `machines` (databases included), `stores`, or `other` for a path no module answers | Info below 400, warn a 4xx, error a 5xx or no answer at all | | **Auth** | An event of auth's audit trail: a sign-in, a sign-up, a password or session change | The event, such as `auth.login.success` | Warn a failure | | **Notifications** | An email, SMS, WhatsApp message or push sent, and what became of it | The channel | Error one that did not arrive (failed, bounced, rejected, expired) | | **Machines** | What a machine wrote, its migration runs included ([Builds, Logs & Rollback](/docs/machines/builds#logs)) | The machine | The level the line says of itself | | **Builds** | A build of a machine's commit | The machine | Error a failed build, warn a cancelled one | Each category is narrowed by text, by source and by level, over the last 15 minutes up to the last 7 days. **Live** reads again every 5 seconds; **Paused** holds the list still while you read it. A line opens whole, each of its fields with a copy button. The address of the page carries the category and the filters, so a view can be shared. ## Request lines A request line holds what the edge knows of the request, and nothing it carried: | Field | | |---|---| | `ts` | When the request arrived | | `module` | The module it went to | | `method`, `path` | The path **without its query**: a signed URL's signature and a token travel in the query, so it is never kept. Past 2,048 bytes it is cut | | `status` | What the client was answered; `0` when it got no answer | | `duration_ms` | Until the last byte of the answer was sent | | `client_ip`, `user_agent` | Who asked: the client's address as the edge saw it, and its user agent (its first 512 bytes) | | `request_id` | The `X-Request-Id` the client was answered with — the one to quote when asking about a request | | `flags` | Present when the request ended short of a module's answer: `UAEX` the key or rate check refused it, `NR` no module answers its path, `UF` or `UH` the module could not be reached, `UT` it did not answer in time, `DC` the client went away | Not its headers, not its body. A line is kept **7 days** and deleted within a day after; only the project it was made for reads it. Anyone can send a project requests, and a refused one is a line too: under a flood, each of the platform's edges keeps about 1 MiB of a project's lines a second and drops the rest, so a flood to one project does not cost the others their lines. From a server, `GET /analytics/requests` with the project's secret key answers the same lines, newest first: | Query | | |---|---| | `window_seconds` | How far back, 1 to 604,800 (a week); an hour when left out | | `module` | One module's requests only | | `level` | `info`, `warn`, `error`, one or more, comma-separated | | `q` | Text found in the path or the user agent, or a request id or a client address, exactly | | `limit` | How many lines at most, 1 to 500 (100 by default) | | `cursor` | The `next_cursor` of the page before; absent on the last page | ```ts // scripts/errors.ts type RequestLine = { ts: string; module: string; method: string; path: string; status: number; request_id: string }; const res = await fetch(`${process.env.PALBASE_URL}/analytics/requests?window_seconds=600&level=error`, { headers: { apikey: process.env.PALBASE_SECRET_KEY! }, }); const { requests } = (await res.json()) as { requests: RequestLine[]; next_cursor?: string }; for (const r of requests) console.log(r.ts, r.status, r.module, r.method, r.path, r.request_id); ``` A secret key never goes in a browser: a call that carries one from a web page is refused (`401 secret_key_in_browser`). # CLI `palbase` is the command line for your organizations, projects and environments. It comes in the same npm package as the SDKs: `npx palbase ` runs it with nothing installed, and in a project that depends on `palbase` it runs the version installed there. A machine is deployed by a push to its branch, which builds and rolls it out ([Builds, Logs & Rollback](/docs/machines/builds)); `palbase machines:deploy` builds the branch's head again, for a push GitHub did not deliver. The CLI does what a push does not — registering client apps and writing their settings, the purchase catalog, an environment's auth, storage, flags and notification settings, tokens for CI and agents, a database's dumps and restores. It is also how a script, a CI job or an AI agent works with Palbase. Every command is in the [CLI reference](/docs/cli/reference). ## Signing in ```sh npx palbase login ``` `login` shows a one-time code and opens Studio, where you approve it while signed in. On a computer with no browser — a server, a container, an agent's sandbox — `palbase login --no-browser` prints the address to open on any device. The CLI then holds a session of yours: Studio lists it beside your other sessions, and ending it there signs the CLI out. `palbase logout` ends it from the terminal, and `palbase whoami` prints who the CLI acts as. ## Choosing an environment Most commands act on one environment of one project. `palbase use` chooses it for the directory you are in: ```sh palbase use shop/main # the project "shop", its environment "main" palbase use acme/shop/staging # when two organizations have a project named "shop" palbase status # who you are there, its machines and databases ``` The choice is kept on this computer, not in the repository, so two checkouts of the same project can each act on their own environment. Commands run in that directory, or one under it, act on it. `-P ` and `-E ` choose for a single command instead: ```sh palbase flags:list -P shop -E staging ``` A change to a production environment asks for confirmation; `--yes` gives it in a script. `--non-interactive` makes a command refuse anything that would ask, and `--json` prints the answer as JSON for a program to read. These are the [global flags](/docs/cli/reference#global-flags) every command takes. ## Projects, environments and keys ```sh palbase projects:create shop # a project with its first environment, main; its keys are printed palbase envs:create staging --from main # another environment of the project chosen here palbase keys:create --kind publishable --label web palbase open auth # the environment's Auth section in Studio ``` `palbase projects:list` and `palbase envs:list` list what you can reach. A new key's value is printed once; `palbase keys:list` lists the environment's keys without their values, and `palbase keys:revoke ` refuses whatever holds one from its next request. Two environments of a project drift apart as settings change in one and not the other. `palbase config:diff --from staging --to main` shows how their module settings differ — auth, storage, flags, notifications, realtime, analytics — and `palbase config:copy --from staging --to main` makes the second equal to the first; secrets are not copied, only named. `palbase types:pull` writes the environment's secret, flag, bucket and role names into `palbase/palbase-env.d.ts`, so `Secrets.get`, `Flags` and `Storage.buckets` accept those names and no others. ## Running a backend on your computer `palbase init` starts a backend on `palbase/backend` in the current directory: a health route, its module, its tests and a Dockerfile; `--db` adds a schema with its routes and their test ([Project Structure](/docs/getting-started/project-structure)). Nothing that already exists is written over. The backend needs the environment's address and a key to run. Two commands give it them: - `palbase env:pull` writes them into `.env.local`, between markers it owns; the rest of the file stays as it is. The key is yours, lasts 30 days, and is revoked by `palbase logout`. No secret's value is written: the app reads the project's secrets when it starts. - `palbase run -- bun dev` runs a command with them in its environment and writes nothing to disk. `palbase test` runs the backend's tests the way the platform runs them before a build takes traffic: test users are made for the run, their tokens are handed to the command, and they are deleted when it ends ([Testing](/docs/backend/testing)). ```sh palbase test --users alice,bob --base-url http://localhost:8080 -- bun test ./tests ``` ## In CI A CI job signs in with an organization's access token instead of a person: set it as the job's `PALBASE_TOKEN` secret, and the CLI acts with it at its role — member or admin — on every project of the organization. ```sh palbase tokens:create github-actions --role admin --days 90 ``` The value is printed once. Tokens are listed with `palbase tokens:list` (never their values) and revoked with `palbase tokens:revoke `; [Access Tokens](/docs/getting-started/access-tokens) says what a token may and may not do. In CI, `palbase apps:sync --check` exits 1 when the app's committed settings and clients are not what the project gives now. ## For an AI agent An agent that works on one environment gets an environment token rather than an organization's: it does what your role does in that environment alone, for a few hours, and never makes or shows a standing credential, changes people or roles, or deletes a project or an environment. ```sh palbase env:tokens:create --minutes 120 --name coding-agent palbase env:tokens:create --scope machines:read,machines:logs # only read machines and their logs ``` Give the agent the value as `PALBASE_TOKEN`. `--scope` narrows it to `machines:read`, `machines:logs`, `apps:sync`, `auth:test-users` or `support`. `palbase env:tokens:list` lists the live ones and `palbase env:tokens:revoke ` ends one at its next request. The docs an agent reads are at `/llms.txt`, with every page in one file at `/llms-full.txt`. ## Searching the docs `palbase docs:search` finds what these docs say about what you need — by its meaning, in your own words; ask in English, the docs' language — and prints the sections that answer it, each with its address and its text: ```sh palbase docs:search "" palbase docs:search "" --limit 3 --json ``` Say what you need as a question of your own — what you are trying to do, not the words of a heading — and the search finds the sections that mean it. It needs no sign-in, and a page changed here is found by what it says now within minutes. A caller searches 10 times in ten seconds, 30 a minute and 300 an hour: a sign-in or `PALBASE_TOKEN`, sent when there is one, as itself, and a search with neither by its address. When many search at once, a search waits its turn for a few seconds, or is told to search again in a moment. `--json` prints what the search answered — what an agent reads. ## Asking Palbase's support line What the docs do not answer — a command or call that fails with nothing to act on, a page that is wrong or missing, something Palbase seems not to do — goes to Palbase's support line: ```sh palbase support:ask "machines:deploy fails with build_failed and the log ends at npm ci — what is wrong?" ``` The answer comes from these docs and from what the environment shows you: its machines, their last builds and the end of a failed build's log, read with your own sign-in or token and sent with the question. When the answer is that Palbase lacks something, the CLI prints a case number: Palbase has it, and you are told by email when Palbase closes it. Keys, tokens and passwords in a question are masked before anyone reads it. An agent asks with an environment token that has the `support` scope. A person asks 30 questions an hour, a project 60. ## What the commands cover | Group | What it does | |---|---| | [`orgs`](/docs/cli/reference#orgs), [`projects`](/docs/cli/reference#projects), [`envs`](/docs/cli/reference#envs) | Organizations and their members, projects, environments ([Organizations](/docs/getting-started/organizations)) | | [`tokens`](/docs/cli/reference#tokens), [`keys`](/docs/cli/reference#keys), [`env`](/docs/cli/reference#env) | Access tokens, an environment's API keys, environment tokens and `.env.local` | | [`apps`](/docs/cli/reference#apps) | The apps that call the project, and the settings and backend clients written into their repositories ([Client Apps](/docs/apps/overview)) | | [`auth`](/docs/cli/reference#auth) | Sign-in providers, settings, roles, hooks, users, the audit trail ([Auth](/docs/auth/overview)) | | [`storage`](/docs/cli/reference#storage), [`flags`](/docs/cli/reference#flags), [`notify`](/docs/cli/reference#notify), [`realtime`](/docs/cli/reference#realtime), [`analytics`](/docs/cli/reference#analytics), [`secrets`](/docs/cli/reference#secrets), [`webhooks`](/docs/cli/reference#webhooks) | Each module's settings and data | | [`purchases`](/docs/cli/reference#purchases) | The catalog, paywalls, the stores' connections and customers ([Purchases](/docs/purchases/overview)) | | [`stores`](/docs/cli/reference#stores) | Builds, testers and review on TestFlight and Google Play ([App Distribution](/docs/distribution/overview)) | | [`github`](/docs/cli/reference#github), [`machines`](/docs/cli/reference#machines) | Connecting GitHub, machines, their builds, logs, crons and tasks ([Machines](/docs/machines/overview)) | | [`databases`](/docs/cli/reference#databases), [`db`](/docs/cli/reference#db) | Databases, their backups, connection addresses, `psql`, dumps and schema plans ([Databases](/docs/machines/databases)) | | [`config`](/docs/cli/reference#config), [`types`](/docs/cli/reference#types) | Comparing and copying module settings between environments, and the environment's names as TypeScript types | | [`docs`](/docs/cli/reference#docs) | Searching these docs by meaning ([Searching the docs](#searching-the-docs)) | | [`support`](/docs/cli/reference#support) | A question to Palbase's support line ([Asking Palbase's support line](#asking-palbases-support-line)) | # CLI reference Every command of `palbase` 0.18.6 — the CLI in the `palbase` npm package — as its own help prints it. Run it with `npx palbase `, or `palbase ` once the package is installed; `palbase help ` prints the same in a terminal. Signing in, choosing an environment and using a token in CI are in [the CLI overview](/docs/cli/overview). 382 commands in 32 groups. ## Global flags Every command takes these. ```text -P, --project the project (name or id; org/project when the name is in two organizations) -E, --env the project's environment (slug or ref) --json print the answer as JSON --non-interactive never ask: refuse what would need a question -y, --yes confirm a change to a production environment without asking --debug print each request the command sends -h, --help show help ``` ## General ### `palbase login` Sign in with your Studio account ```sh palbase login ``` Shows a one-time code and opens Studio, where you approve it while signed in there. The CLI then holds a session of yours: Studio lists it with your others, and ending it there signs the CLI out. On a machine with no browser (a server, an agent), open the address it prints on any device. ```text --no-browser print the address instead of opening a browser ``` ### `palbase logout` Sign out: the session is ended at Palbase and forgotten on this computer ```sh palbase logout ``` ### `palbase whoami` The account the CLI acts as, and the environment chosen for this directory ```sh palbase whoami ``` ### `palbase use` Choose the environment this directory acts on (kept on this computer) ```sh palbase use [[/] | //] [--clear] ``` Commands run in this directory, or a directory under it, then act on that environment unless -P/-E say otherwise. Nothing is written into the project: two checkouts of it can each act on their own environment. With no argument and a person at the terminal, it lists your environments to pick from. ```text --clear forget the choice made for this directory ``` ### `palbase status` The environment this directory acts on: who you are there, its machines and databases ```sh palbase status ``` ### `palbase open` Open the environment in Studio (its overview, or a section: auth, storage, flags, …) ```sh palbase open [section] ``` ### `palbase init` Start a backend on palbase/backend here (or in ``): a health route, its module, its tests, a Dockerfile; --db adds a schema, its routes and their test ```sh palbase init [dir] [--db] [--print] ``` Nothing already there is written over: a file init would write that exists stops it, naming the file. --db's table is typed in palbase/palbase-env.d.ts by the block palbase db:types writes of the schema: run db:types again when you change it. --print writes nothing: it prints the files as JSON ({ name, db, version, files: { ``: `` } }), for a program that seeds a repository with this release's starter (bunx `palbase@` init --print `` --db). Then: bun install, palbase use `/`, palbase env:pull, bun dev. Its tests: bun test (here, no app); palbase test --users alice,bob --base-url http://localhost:8080 -- bun test ./tests (against the app you started, as test users made for the run); on the platform, palbase machines:update `` --test-command "bun test ./tests" --test-users alice,bob runs them against each new build before it takes traffic. ```text --db with a database: a schema file (a table each user reads alone) and its routes --print print the files as JSON, write nothing ([dir] names the package) ``` ### `palbase strings` Keep the app's translations (palbase/strings/) with its code: each t("…") a sentence, every language's file with it ```sh palbase strings [--source ] [--add ] [--translate] [--prune] ``` Reads every t("…") the backend's source calls — imported from palbase/backend, or from a module of the app that re-exports it — in the app's directory (the nearest one up with a package.json). A sentence new in the code is added to each language's file, empty: t() answers it in the source language until it is translated. A translation is kept. A sentence the code no longer calls is kept and listed; --prune takes it out of every file. A call it could not follow (a t from a module it does not read, a linked directory) is said, where. palbase/strings/_meta.json names the source language (the one the sentences are written in); each other language is `palbase/strings/.json`, "sentence": "translation". Edit them as any file of the app. --translate has the environment's ai module translate what is empty (the project's AI use, as any other: its cost is in the AI usage; production's asks first). A translation is used as it is written; correct it in the file. A table in the earlier format (version 2) is written in this one. ```text --source the language the sentences are written in, for a table not made yet (tr, en, pt-BR) --add a language to translate into: its file is made (may be given more than once) --translate translate what is empty, with the environment's ai module (-P/-E, or palbase use) --prune take the sentences the code no longer calls out of every file ``` ### `palbase run` Run a command with the environment's address and your own key in its environment, nothing written to disk ```sh palbase run [--secrets development|production] -- [args…] ``` palbase run -- bun dev, palbase run -- npm test: the app reads the project's secrets at boot (PALBASE_SECRETS). ```text --secrets the project's secrets the app reads at boot: development (default) or production ``` ### `palbase test` Run the backend's tests as the platform does before a build goes out: test users made for the run and deleted after, `PALBASE_TEST_*` in the command's environment ```sh palbase test [--machine ] [--users ] [--base-url ] [--secrets development|production] [-- [args…]] ``` palbase test --machine backend --base-url http://localhost:8080: the machine's own test command and test users, as the platform runs them before each build of it rolls out. palbase test --users alice,bob --base-url http://localhost:8080 -- bun test: api.signInAs("alice") acts as a test user made for this run. The command gets what palbase run gives it, and PALBASE_TEST_AUTH_URL (the environment's address), PALBASE_TEST_IDENTITIES (the run's test users, each with its token) and, with --base-url, PALBASE_TEST_BASE_URL (where the app under test answers; without it HTTP tests skip themselves). The test users are deleted when the command ends, however it ends — Ctrl-C too: the command is stopped, they are deleted, and palbase test ends as the signal would have. ```text --machine a machine whose test command (when no command is given) and test users (when --users is not) are run --users the test users to make for the run, comma-separated --base-url where the app under test answers (http://localhost:8080) --secrets the project's secrets the app reads at boot: development (default) or production ``` ### `palbase billing` An organization's card and each environment's plan, balance, month and limit ```sh palbase billing [--org ] ``` A member reads the plans and what was used; the money (balances, amounts) is an owner's or an admin's. ```text --org the organization (name or id; the one of the environment chosen here, or your only one) ``` ### `palbase journeys` The environment's journeys: each one's key, name, status, way in, and when it was published ```sh palbase journeys [--status draft|live|stopped] ``` ```text --status draft, live or stopped alone ``` ## `env` ### `palbase env:pull` Write what the backend needs to run here into .env.local: the environment's address and your own key — no secret's value ```sh palbase env:pull [--file ] [--secrets development|production] ``` The app reads the project's secrets at boot (PALBASE_SECRETS); their names are listed as comments: set one in the file to override it. The key is yours (CLI — you (this computer)), good for 30 days, made again by env:pull and run; palbase logout revokes it. Only the block between the palbase markers is written; the rest of the file stays as it is. ```text --file the file (default .env.local) --secrets the project's secrets the app reads at boot: development (default) or production ``` ### `palbase env:tokens:list` The environment's live tokens, by their hint (never their values) ```sh palbase env:tokens:list ``` ### `palbase env:tokens:create` Mint a token of the environment for a program (an AI agent): your role in this environment alone, a few hours; shown this once ```sh palbase env:tokens:create [--scope [,]] [--minutes ] [--name ] ``` It does in this environment what your role does — as your own CLI would — but never makes or shows a standing credential (a key, a token), changes people or roles, or deletes a project or an environment; nothing beyond this environment. --scope narrows it to: machines:read, machines:logs, apps:sync, auth:test-users, support — machines:read reads the machines, their builds, crons and queues; machines:logs their logs; apps:sync what palbase apps:sync reads; auth:test-users makes and deletes test users; support asks Palbase's support line (palbase support:ask). Give it to the program as PALBASE_TOKEN: the CLI acts in this environment alone with it. It ends after --minutes (default 60, at most 240), and with what minted it. ```text --scope narrow it to: machines:read, machines:logs, apps:sync, auth:test-users, support (comma-separated, or given again); none: your role --minutes how long it lives (default 60, at most 240) --name what it is for, as the environment lists it ``` ### `palbase env:tokens:revoke` Revoke an environment's token (by its id; with PALBASE_TOKEN an environment's token, `self` revokes it): refused from its next request on ```sh palbase env:tokens:revoke ``` ## `types` ### `palbase types:pull` Write the environment's secret, flag, bucket and role names into palbase/palbase-env.d.ts as types: Secrets.get, Flags and Storage.buckets take them and no other ```sh palbase types:pull [--file ] ``` The names of the production secrets — what the machines start with, and so what Secrets.get reads there; development's are a developer's — leaving out what the platform sets itself (PALBASE_SECRET_KEY …), every flag, each bucket with its renditions (`Storage.buckets..getPublicUrl(path`, { variant })) and every role — never a value. Only the block between the palbase:stack markers is written; the rest of the file stays as it is. Run it again when a name is added or taken away. ```text --file the file (default palbase/palbase-env.d.ts) ``` ## `orgs` ### `palbase orgs:list` Your organizations and your role in each ```sh palbase orgs:list ``` ### `palbase orgs:create` Make an organization; you are its owner ```sh palbase orgs:create ``` ### `palbase orgs:members:list` An organization's members, and the invitations still open (for its admins) ```sh palbase orgs:members:list ``` ```text --org the organization (name or id); needed when you are in more than one ``` ### `palbase orgs:members:invite` Invite someone to an organization by email; they accept in Studio ```sh palbase orgs:members:invite [--role admin|member] ``` ```text --org the organization (name or id) --role admin or member (default member) ``` ### `palbase orgs:members:remove` Take a member (by email or user id) or an open invitation out of an organization ```sh palbase orgs:members:remove ``` ```text --org the organization (name or id) ``` ### `palbase orgs:audit` Who changed what at an organization's environments, through Studio or the CLI, newest first — its admins' ```sh palbase orgs:audit [--org ] [--limit ] ``` A module keeps a change made through Studio as Studio's key's: this says whose it was. What was sent is not kept. ```text --org the organization (name or id) --limit how many at most (default 50) ``` ## `tokens` ### `palbase tokens:list` An organization's access tokens (PALBASE_TOKEN for CI and agents): name, role, when they end and were last used — never their values ```sh palbase tokens:list ``` ```text --org the organization (name or id) ``` ### `palbase tokens:create` Make an access token of an organization for CI or an agent; its value is shown this once ```sh palbase tokens:create [--role member|admin] [--days ] ``` Give it to the CI as the secret PALBASE_TOKEN: the CLI acts with it in place of a sign-in. It reaches its organization's projects and environments at its role, and nothing of its people or tokens. It ends after --days (default 90, at most 365), and goes when it is revoked or when the one who made it leaves the organization. ```text --org the organization (name or id) --role member (default) or admin: what it may change --days how many days it is good for (default 90) ``` ### `palbase tokens:revoke` Revoke an organization's access token (by its name or id): refused from its next request on ```sh palbase tokens:revoke ``` ```text --org the organization (name or id) ``` ## `billing` ### `palbase billing:charges` A month's charges, by environment and item — used, at the price table, charged past each one's free quota — as a table or as CSV ```sh palbase billing:charges [--period ] [--csv] [--org ] ``` --csv prints the CSV Studio downloads: what an organization bills its own customers by. ```text --org the organization (name or id; the one of the environment chosen here, or your only one) --period the month (this one by default) --csv print the charges as CSV ``` ### `palbase billing:invoices` The invoices of an organization's top-ups, each of an environment: Stripe makes one of each, and emails it ```sh palbase billing:invoices [] [--org ] ``` ```text --org the organization (name or id; the one of the environment chosen here, or your only one) ``` ### `palbase billing:ledger` Every change of an environment's balance, newest first: top-ups, usage taken, credit moved, refunds, what expired ```sh palbase billing:ledger [] [--limit ] ``` ```text --limit how many at most (default 50) ``` ### `palbase billing:env` An environment's place in the bill — its plan, frozen, its month's limit, its automatic top-up — read, or changed ```sh palbase billing:env [] [--plan free|pay_as_you_go] [--limit |none] [--frozen yes|no] [--auto-topup yes|no] [--threshold ] [--amount ] [--low-balance-days ] [-y] ``` With no flag it shows the environment: its plan, balance, automatic top-up and month. Each environment pays for itself: at its limit, or its balance run out, it is Free until the month ends or a top-up comes; the organization's other environments go on. Moved to Pay as you go it needs credit to pay from: its first top-up (palbase billing:topup) moves it. Frozen, its requests are refused and its machines and databases stopped, its data kept; only what it stores is charged. Freezing asks first; -y does not. ```text --plan Free (charged nothing, held to the free quotas, its credit kept) or Pay as you go (paid from its balance) --limit at most this much a month; none for no limit --frozen frozen: its requests refused, its machines and databases stopped, its data kept --auto-topup take a top-up from the organization's card when the balance is under the threshold --threshold the balance under which it is taken --amount how much it takes ($10 at least) --low-balance-days email the owners when the balance lasts fewer than n days of spend (1 to 60) ``` ### `palbase billing:topup` Put money on an environment's balance now — from what the organization's bank transfers left, else its saved card: $10 to $10,000, the tax on top, an invoice made of it ```sh palbase billing:topup [] [-y] ``` It takes the money: it asks first, and -y does not. An environment on Free is moved to Pay as you go by it (its first top-up). With no card saved yet, the first top-up is on Stripe's page, in Studio. ### `palbase billing:bank-transfer` An invoice of an environment's top-up to pay by bank transfer, to the organization's own bank details: credited as the money arrives ```sh palbase billing:bank-transfer [] ``` Stripe emails the invoice with the bank details; nothing is taken until the transfer arrives. A transfer of more than the invoice stays with the organization and pays its environments' next top-ups, before any card — for 75 days, then Stripe sends what is left back. ### `palbase billing:transfer` Move credit from one of an organization's environments to another — all of it, or an amount — each part with its expiry ```sh palbase billing:transfer [] [-y] ``` An owner's or an admin's (an admin's access token too); never to another organization's environment. It asks first, and -y does not. A deleted environment keeps its credit until it is purged (a week): move it before then, or it is lost. ## `projects` ### `palbase projects:list` Every project of your organizations, with its environments ```sh palbase projects:list ``` ### `palbase projects:create` Make a project with its first environment (main), and print its keys ```sh palbase projects:create [--org ] [--env-name ] ``` The first environment is the project's production one: its slug is always main. Its secret key is printed this once; keys:create makes more. ```text --org the organization (name or id) --env-name what the first environment is called on screen (default Production) ``` ### `palbase projects:delete` Delete a project and every environment of it ```sh palbase projects:delete [-y] ``` Its environments' data goes with them (the platform keeps it a week to bring back). ## `envs` ### `palbase envs:list` A project's environments (the project chosen here, or -P) ```sh palbase envs:list ``` ### `palbase envs:create` Add an environment to a project (staging, a developer's own) ```sh palbase envs:create [--name ] [--from [--with-cloud]] ``` Each environment is a project of its own on the platform: its users, data, machines and keys are its own. --from opens it with another environment's module settings (palbase config:copy: secrets are named, not copied); --with-cloud its machines and databases too — declared alike, built from the same branches, with no data (palbase db:copy brings it). ```text --name what it is called on screen (default: the slug) --from open it with this environment's settings (slug or ref) --with-cloud with --from: its machines and databases too (the new environment's GitHub link builds the machines) ``` ### `palbase envs:delete` Delete an environment of a project, with its data ```sh palbase envs:delete ``` ## `keys` ### `palbase keys:list` The environment's API keys (never their values) ```sh palbase keys:list ``` ### `palbase keys:create` Make an API key of the environment; its value is shown this once ```sh palbase keys:create --kind publishable|secret [--label