Skip to content

Latest commit

 

History

History
235 lines (185 loc) · 10 KB

File metadata and controls

235 lines (185 loc) · 10 KB

Contributing to Baselyra

Baselyra is small on purpose. The most valuable contribution is usually the one that removes something, or that makes an existing thing correct in a case it currently gets wrong.

Before you start

  • Bug report — open an issue with the version (GET /health), what you did, what happened, and the smallest reproduction you can manage. A failing curl is ideal.
  • Feature — open an issue first. Anything that adds a runtime dependency, a container, or a concept will be declined; see Non-negotiables.
  • Security — do not open a public issue. See Security.

Layout

src/
  index.ts            Fastify bootstrap; mounts every prefix, serves the Studio
  config.ts           All configuration, read from the environment
  db.ts               two pools: pool (project) + controlPool (control database);
                      query(), tx(), asRole()  ← route user data through asRole
  jwt.ts              signJwt / verifyJwt / mintProjectKeys
  context.ts          Caller from headers; projectUserId()
  errors.ts           ApiError and the SQLSTATE → HTTP map
  auth/               /auth/v1 — routes, service, password hashing
  rest/               /rest/v1 — catalog introspection and the query compiler
  storage/            /storage/v1 — buckets, objects, signed URLs
  realtime/           ws /realtime/v1 — change feeds, broadcast, presence
  admin/              /admin/v1 — the Studio's API, schema tooling
  platform/           Studio accounts: control.platform_users, tokens, roles
  import/             /admin/v1/import — Supabase, Postgres, Appwrite, Firebase, SQL dumps
  ai/                 /ai/v1 — the DeepSeek relay
  mail/               SMTP and template rendering
  metrics/            request metering, flushed to the control database
db/project/           *.sql for the project database (auth, storage, public)
db/control/           *.sql for the control database (Studio accounts, audit)
studio/               React 19 + Vite + Tailwind v4 admin console
sdk/js/               @baselyra/client — zero-dependency JS/TS client
scripts/              setup, migrate, smoke, backup, restore
deploy/               nginx and Apache vhosts
test/                 node:test, one file per area

Running from source

You need Node 22+ and a Postgres 17 you can reach.

git clone https://github.com/baselyra/baselyra.git
cd baselyra
npm install
cp .env.example .env      # or ./scripts/setup.sh

Point DATABASE_URL at your database. The quickest one is the compose file's:

docker compose up -d db
# then in .env:
DATABASE_URL=postgres://baselyra:<password>@127.0.0.1:5432/baselyra

(That needs the db port published — add a compose.override.yml with ports: ["127.0.0.1:5432:5432"]; the shipped file deliberately publishes nothing.)

node scripts/migrate.js       # creates both databases, applies db/project + db/control,
                              # creates the first Studio account
npm run dev                   # node --watch --experimental-strip-types src/index.ts

The server listens on PORT (3000 by default). The Studio's dev server proxies to 127.0.0.1:3000, so leave that port alone while developing the frontend:

cd studio
npm install
npm run dev                   # http://localhost:5173

Build both the way the Docker image does:

npm run build                 # tsc -> dist/
cd studio && npm run build    # tsc --noEmit && vite build -> studio/dist/

Tests

npm test                      # node --experimental-strip-types --test test/*.test.ts

Every test runs with no database, no build step and no environment. That is a hard rule: the pure logic worth testing — filter parsing, JWT handling, password hashing, path safety, template rendering, platform role decisions, the import module's safety rules — must be reachable on a bare checkout.

Two consequences you will hit immediately:

  • Test files import ../src/foo/bar.ts with a .ts extension. node --test strips types in place; it does not remap a .js specifier onto a TypeScript file the way a bundler does.
  • A module that cannot be loaded without a database is a module whose logic wants extracting. src/import/types.ts is the pattern: it imports nothing local at all, which is what lets its rules be tested directly.

Add one runnable check per piece of non-trivial logic you write. Not a suite, not fixtures, not a framework — the smallest thing that fails if the logic breaks.

Against a running instance, the end-to-end check is:

./scripts/smoke.sh http://127.0.0.1:3130 admin@example.com 'password'

It exercises auth, REST, storage, realtime, the admin API and the Studio, and cleans up after itself. A change is not finished until it passes.

Non-negotiables

These are the constraints that keep Baselyra worth choosing. A pull request that breaks one will be declined however good it is otherwise.

Dependencies are frozen. The server has exactly eight: fastify, @fastify/cors, @fastify/rate-limit, @fastify/multipart, @fastify/websocket, @fastify/static, pg, nodemailer. The Studio has react, react-dom, react-router-dom and Tailwind v4. The client SDK has none. Use node:crypto, node:fs/promises, fetch and the rest of the standard library, or write the twenty lines by hand.

Postgres enforces authorisation. Never filter rows in JavaScript for security. User-facing queries go through asRole() and RLS decides. A route handler that checks ownership itself is a bug even when it happens to be correct, because the same data is reachable through three other paths.

Every SQL value is a bound parameter. Identifiers are validated against the catalog or a strict regex and quoted with quoteIdent. There is no third option. If you find yourself interpolating a request value into a statement, stop.

TypeScript, ESM, strict, noUncheckedIndexedAccess. Local imports in src/ carry a .js extension (NodeNext). The Studio uses bundler resolution and no extension.

No placeholders. No TODO, no stubbed handler, no mock data, no "coming soon". If it is in the tree, it works against a real database.

Studio colours come only from the CSS variables in studio/src/styles.css, written as bg-[var(--panel)], text-[var(--text-muted)], border-[var(--border)]. No hex, no Tailwind palette colours — the theme switch depends on it. The rest of the design contract is in STUDIO.md.

Comments explain why, not what. Do not narrate obvious code. No banner comments, no ===== dividers. A comment earns its place by recording a decision someone would otherwise undo.

Style

  • Small modules with one job. Routes unpack and shape; decisions live in a service module beside them.
  • Errors come from src/errors.ts and are thrown, not returned. The global handler serialises them as { error: { code, message, details } }.
  • Prefer an explicit if over a clever expression. The person reading it is on-call at 3am.
  • Studio pages are lazy-imported by App.tsx and default-export a component named after the file. They import from ../components/ui, ../components/icons, ../components/AppLayout, ../lib/hooks, ../lib/types, ../lib/format and ../lib/api — never fetch directly.
  • After any DDL from the Studio, call invalidateSchema() from ../lib/hooks.

Database changes

Add a new numbered file in db/project/ or db/control/, whichever database the change belongs to. Never edit an applied migration in a release that shipped.

Which one: anything the application uses — auth, storage, public, the project's baselyra configuration — is a project migration. Anything about operating Baselyra — Studio accounts, the audit log, import history, request metering — is a control migration. If a new feature stores operator state, it goes in the control database and reaches it through controlQuery() / controlTx(), never query(). Putting it in the project database would hand it to the SQL editor and to anything holding the service key.

Every file must be re-runnable: create table if not exists, drop policy if exists before create policy, catalog checks before an alter. scripts/migrate.js checksums each file and re-runs one that changed, which is the intended way to evolve the schema during development — and it is only safe because of that rule.

Each file runs inside one transaction, so a migration that fails halfway leaves nothing behind.

If your change creates a table in public, enable RLS in the same file. ALTER DEFAULT PRIVILEGES grants anon and authenticated full DML on new tables there, so a table without RLS is world-writable the moment it exists.

Commits and pull requests

  • One logical change per pull request.
  • Present-tense subject describing the change: reject unfiltered DELETE, not fixed stuff.
  • Say what you ran: npm test, scripts/smoke.sh, manual steps.
  • Update the docs in the same pull request. A behaviour change with stale docs is an incomplete change.
  • New behaviour that a self-hoster can misconfigure gets a line in the relevant docs/ page, not just a code comment.

Security

Do not open a public issue for a vulnerability. Email the maintainers privately with a description, an assessment of impact, and a reproduction. You will get an acknowledgement, and a fix or an explanation.

Areas where a report is especially welcome:

  • Anything that reads or writes rows RLS should have hidden.
  • Anything that lets a project user reach /admin/v1, or turns an application token into a platform one.
  • Path traversal or symlink escape in storage keys.
  • SQL injection through an identifier path — a column name, a table name, a sort column, a cast.
  • Token handling: reuse detection, session revocation, signature verification.
  • A credential appearing in a log, an audit row, an error message or an import progress stream.

Licence

By contributing you agree that your contribution is licensed under the Apache License 2.0, the same as the project. See LICENSE.