Documentation

Zulivio Documentation

Everything you need to install, configure, and operate Zulivio for your team. Scroll through — every section is on this one page.

Introduction

Zulivio

Open-source, self-hostable CRM and workforce-operations platform. One Docker install, no subscription, no per-seat fee, no data leaving your server.

AGPL-3.0DockerNestJSNext.jsPostgreSQLPrisma

What is Zulivio?

Zulivio is a role-based employee management, work-assignment, and attendance-tracking platform that runs entirely on infrastructure you control. It ships as a two-service Docker stack — a Next.js frontend and a NestJS backend backed by PostgreSQL — and is accessible from any device on your network once it’s running. There is no hosted version and no telemetry: the only outbound calls the app ever makes are ones you explicitly configure, like the optional Google Sheets sync.

It is currently the workforce-operations core of a much larger planned CRM. What’s shipped today — employees, assignments, attendance, knowledge base, dashboard, CSV/Sheets — is real and tested, not a preview. A full sales pipeline, marketing, and customer-service workspaces are on the public roadmap but not yet built; see Scope & Limitations for the exact line between the two.

Quick orientation

If this is your first time, follow these three steps:

  1. Install Zulivio — one command, a few minutes.
  2. Understand the role hierarchy — who can do what.
  3. Add your first employees — generates credentials automatically.

Key capabilities

  • Role-based access control — Master Owner, Company Admin, Sales Head, Manager, Employee, enforced server-side on every request, never just a hidden button.
  • Employee lifecycle — add, edit, reset password, and remove, all rank-guarded so no one can promote themselves or a peer.
  • Work assignments — a guarded status pipeline with a full transition audit trail and outcome notes.
  • Attendance tracking — an explicit shift/break state machine with server timestamps, not a self-reported timesheet.
  • Knowledge base & tips — versioned PDF training with per-employee acknowledgement tracking, plus a daily team tip.
  • Live master dashboard — headcount, assignment status, overdue work, and who’s on the clock right now.
  • CSV & Google Sheets — import/export with formula-injection protection, and a real (credential-gated) Sheets adapter.
  • Self-hosted — your data stays on your own server, in your own PostgreSQL database.

Browse the documentation

Architecture at a glance

Browser
      │  http://<server>:3100   ← only exposed port
      ▼
┌─────────────────┐  /api/* proxied  ┌──────────────────┐
│  Next.js :3100  │ ───────────────▶ │  NestJS :4100    │
│  (web)          │ ◀─────────────── │  + Prisma        │
└─────────────────┘  server-side     └──────────┬───────┘
                                                 ▼
                                    PostgreSQL (Docker volume)

The web service is the only container exposed on your network. The backend and database live entirely on the internal Docker network — neither is reachable from the LAN directly. All session cookies are first-party and httpOnly. See Architecture for the full picture.

License & attribution

Zulivio is released under the GNU Affero General Public License v3.0 (AGPL-3.0). It is free to use, self-host, fork, modify, and redistribute — including for commercial use. If you run a modified version as a network service for others, you must make that modified source available to those users under the same license. Built and maintained by NODEDR INFOTECH PRIVATE LIMITED.

Getting Started

Installation

Get Zulivio running on a server in a few minutes. The install script is the only prerequisite you need — it checks for everything else.

One-command install (recommended)

Clone the repository and run the install script. It checks for Docker, generates a .envfile with a random PostgreSQL password (only if one doesn’t already exist — safe to re-run), builds both images, starts the stack, waits for it to report healthy, and prints the URL to open.

git clone https://github.com/Raktim94/zulivio.git && cd zulivio && ./install.sh

Re-run ./install.sh any time — for example after git pull — to rebuild and restart. It never touches existing data.

Manual install

If you’d rather run the steps yourself:

cp .env.example .env
# Edit .env and set POSTGRES_PASSWORD to a real value — required,
# compose refuses to start without it.
# Generate one with: openssl rand -base64 32

docker compose up --build

First launch — creating your organization

Once the stack is healthy, open http://localhost:3100/setup and create your organization. This is a one-time step that creates your company and its first Master Owneraccount. There is no baked-in demo password — you choose the Master Owner’s password during setup.

Closing self-service sign-up: once your organization is set up, set BOOTSTRAP_DISABLED=true in .env and restart the backend service to permanently disable the /api/v1/bootstrap endpoint. Recommended for a single-tenant deployment so a second organization can never be created on the same instance.

Changing the port

The default port is 3100. Edit HOST_PORT and FRONTEND_ORIGIN together in .envdocker-compose.yml reads this file automatically, so the compose file itself never needs editing:

# .env
HOST_PORT=4000
FRONTEND_ORIGIN=http://localhost:4000

The two values must agree — FRONTEND_ORIGIN is used for CORS so the backend only accepts requests from the correct origin. Then restart: docker compose up -d.

Deploying to a VPS or cloud server

Zulivio runs the same way on a rented cloud server as it does on an office LAN box — the setup, the data, and the login are identical. The only difference is the values in .env:

# .env — publicly reachable host
HOST_PORT=3100
FRONTEND_ORIGIN=https://crm.yourdomain.com

Put a reverse proxy with real HTTPS in front (Caddy, Nginx + Let’s Encrypt, or a managed proxy), point it at HOST_PORT, then run docker compose up -d.

Reaching it from outside your LAN

To make Zulivio reachable outside your office network without exposing a port directly, put it behind a tunnel — Cloudflare Tunnel, ngrok, Tailscale Funnel, or any reverse proxy you already run. None of these ship inside Zulivio’s own docker-compose.yml today; you run them alongside it, pointed at HOST_PORT. Once traffic reaches the app over https://, set your session cookie to Secure at the proxy/CORS layer to match — a plain http:// LAN setup should leave this off.

Accessing from other devices on the network

Any device on the same network — a second desktop, a manager’s laptop, a tablet — can open Zulivio by navigating to:

http://<IP-of-the-machine-running-Docker>:3100

Find your machine’s LAN IP with ip addr (Linux), ipconfig (Windows), or System Preferences → Network (macOS). No per-device install — the browser is the client.

Stopping, restarting, and updating

# Stop containers (data is preserved in named volumes)
docker compose down

# Start again
docker compose up -d

# Stop AND delete all data (destructive — use only to start fresh)
docker compose down -v

# Update to a new version
git pull
docker compose up -d --build

Or re-run ./install.sh — it does exactly the same rebuild-and-restart, and never touches your data.

Getting Started

Roles & Access Control

A strict five-level hierarchy enforced on the server for every request — not a permission checkbox in the UI.

The hierarchy

RoleTypical useCan do
Master OwnerCompany ownerEverything, including creating Company Admins
Company AdminOperations leadEverything except creating other admins/owners
Sales HeadHead of salesManage managers/employees, assignments, reports
ManagerTeam leadAdd/remove employees below them, assign work, view team reports
EmployeeFront-line staffOwn attendance, own assignments, knowledge base, tips

How enforcement works

Every request is checked against this hierarchy in the backend — a NestJS guard plus per-service checks — never only by hiding a button in the interface. Concretely:

  • A manager can never create, edit, or promote a peer or a higher role — privilege escalation is blocked on both create and edit, and covered by the automated test suite.
  • An employee can never view another employee’s attendance or performance report.
  • Every business record carries an organizationId; every query is scoped to the organization from the authenticated session, never from client input — one Zulivio instance can safely host more than one organization’s data with no cross-tenant leakage.

This is tested, not just claimed: apps/backend/test/app.e2e-spec.ts runs 26 end-to-end tests against a real PostgreSQL database (not mocked), covering privilege escalation attempts, cross-employee report access, the full attendance state machine, and the full assignment lifecycle. All 26 pass at the last run.

Using Zulivio

Employee Management

Add, edit, and remove employees with auto-generated credentials — every action rank-guarded server-side.

Adding an employee

Creating an employee (Manager rank or above) automatically generates a unique employee number (EMP-0001, EMP-0002, …) and a random temporary password. The password is shown exactly once, at creation time, in the UI — it is never stored in plaintext and can never be retrieved again afterward.

Editing an employee

Update a subordinate’s role, department, or employment status — including reactivating someone SUSPENDED or ON_LEAVE — from the Employees page. Guarded the same way as creation: you can never promote someone to your own rank or above, even by accident.

Resetting a password

Force-generates a new temporary password for a subordinate and immediately revokes all of their active sessions — the reset takes effect right away, not on their next natural login.

Removing an employee

Removing an employee marks them SEPARATED and immediately revokes all their sessions. This is a soft action: their history is preserved for reporting and audit, there is no hard delete, and employee numbers are never reused.

Visibility scope

Manager rank and above have full visibility into every assignment, attendance record, and report in the organization. An Employee, or a Manager scoped to a specific team, sees only their own record or their direct reports’ — never the whole organization by default.

Using Zulivio

Work Assignments

Create work, assign it to a specific employee, and move it through a guarded status pipeline with a full audit trail.

Creating and assigning work

Create a piece of work and assign it to a selected employee by employee number. Reassignment works the same way — assign the same piece of work to a different employee number at any point in its lifecycle.

The status pipeline

Every assignment moves through an explicit, guarded pipeline:

ASSIGNED → IN_PROGRESS → FOLLOW_UP / BLOCKED → COMPLETED / CANCELED

Transitions are validated server-side. An invalid jump — for example, skipping straight from ASSIGNED to COMPLETED, or mutating an assignment that’s already in a terminal state — is rejected, not silently allowed.

Audit trail

Every status transition is recorded with who made it, when, and an optional outcome note, giving a full, tamper-evident history of how a piece of work moved from assigned to done — not just the current status.

Using Zulivio

Attendance Tracking

An explicit shift/break state machine with server timestamps — not a self-reported timesheet.

The state machine

logged_out → working → on_break → working → logged_out

Every transition is timestamped by the server, not the client. Only one open session is allowed per employee at a time — starting a second shift while one is already open is rejected. If an employee ends their shift while a break is still open, that dangling break is automatically closed rather than left hanging.

Employee reports

Every employee’s report shows:

  • Login and logout times for every shift.
  • Total worked minutes and total break minutes.
  • A per-session breakdown, not just a daily total.
  • Assignment counts by outcome — completed, follow-up, blocked, in-progress.
  • Training acknowledgement status.

The same report is visible to the employee and to their manager — not a one-sided log the employee can’t see. There is no keylogging, no screenshot capture, and no webcam monitoring; attendance is measured by shift/break state, nothing else.

Using Zulivio

Knowledge Base & Tips

PDF training documents with a draft/publish lifecycle and per-version acknowledgement tracking, plus a daily team tip.

Uploading and publishing documents

Managers and above can upload a PDF training document. New uploads start as a draft— invisible to anyone else until explicitly published. Publishing makes it visible to whichever audience it’s assigned to.

Training assignments

Assign a published document to a role (e.g. every Employee) or to a specific person. Each employee’s acknowledgement is tracked per version — if a document is updated and republished, employees who acknowledged the previous version are asked to acknowledge the new one.

Today’s tips

A daily tip feed appears on every employee’s front page — a lightweight way to surface a reminder or a piece of process guidance without requiring a full document read.

Using Zulivio

Dashboard & Reports

One screen for the whole organization's operational health, updated live.

Master dashboard

The dashboard (GET /api/v1/reports/dashboard) surfaces:

  • Current headcount.
  • Assignments broken down by status.
  • An overdue-assignment count.
  • A live board of who’s currently working or on break.
  • Knowledge-base stats — documents published, acknowledgement completion.

Per-employee reports

GET /api/v1/reports/employees/:employeeId combines the attendance report and assignment outcome counts for one employee into a single view — the same evidence a manager and the employee themselves both see.

Using Zulivio

Data Hub & Google Sheets

CSV import/export with formula-injection protection, plus a real, credential-gated Google Sheets adapter.

CSV export

Export the employee directory or the full assignment list as CSV at any time. Exports are sanitized against formula injection — a cell value starting with =, +, -, or @is neutralized before it reaches the file, so opening the export in Excel or Sheets can’t trigger an unexpected formula.

CSV import

Bulk-import employees from a CSV file (Manager rank or above). Errors are reported per row rather than failing the whole import — you get a clear list of which rows succeeded and which need fixing.

Google Sheets integration

The Sheets adapter is real, not a mock — it only activates once you provide credentials, per the project’s “no fake integrations” rule. Without credentials, GET /api/v1/integrations/google-sheets/status returns { configured: false } and the Data Hub page says so plainly — it never pretends the integration works.

To enable it:

  1. Create a Google Cloud service account and enable the Sheets API.
  2. Generate a JSON key and set GOOGLE_SHEETS_CLIENT_EMAIL / GOOGLE_SHEETS_PRIVATE_KEY in .env — the private key needs its newlines escaped as \n in the .env file.
  3. Share your target spreadsheet with the service account’s email address.
  4. Restart the backend service. The Data Hub page shows “Connected” and exposes live import/export.
Administration

Environment Variables

The full list lives in .env.example — these are the ones that matter for day-to-day operation.

VariableRequiredPurpose
POSTGRES_PASSWORDYesShared by postgres/migrate/backend
FRONTEND_ORIGINNoCORS allow-origin (default http://localhost:3100)
HOST_PORTNoHost port for the web app (default 3100)
BOOTSTRAP_DISABLEDNoSet true to close self-service org creation
GOOGLE_SHEETS_CLIENT_EMAIL / GOOGLE_SHEETS_PRIVATE_KEYNoEnables live Sheets sync
SEED_MASTER_OWNER_PASSWORDOnly for pnpm db:seedNever baked into the image

./install.sh generates POSTGRES_PASSWORD for you automatically on first run — you only need to touch this file for a non-default port, a public domain, or the optional Sheets integration.

Administration

Backup & Restore

All data lives in PostgreSQL, inside a named Docker volume — not inside the containers.

# Backup the database
docker compose exec postgres pg_dump -U nodedr zulivio | gzip > backup-$(date +%F).sql.gz

# Restore
gunzip -c backup-2026-08-12.sql.gz | docker compose exec -T postgres psql -U nodedr zulivio

# Run migrations manually (also run automatically by the migrate one-shot
# service on every startup)
docker compose exec backend npx prisma migrate deploy

# Tail logs
docker compose logs -f backend web
Data safety: stopping, removing, or rebuilding containers never touches your data — it lives in named Docker volumes. Only docker compose down -v deletes it, and that requires an explicit, separate command.
Administration

CasaOS / ZimaOS

A CasaOS App Store manifest ships in the repository for home-server installs.

casaos/docker-compose.yml is the CasaOS manifest (x-casaos metadata, bind-mounted /DATA/AppData/$AppID/... volumes per CasaOS convention). It differs from the plain compose.yaml in two ways:

  • Migrations run inline in the backend’s startup command (prisma migrate deploy && node dist/src/main.js) instead of a separate one-shot service, since CasaOS doesn’t cleanly support init containers — safe because migrate deploy is idempotent.
  • Images are referenced by tag (ghcr.io/raktim94/zulivio-*) and built/published for both amd64 and arm64 by a GitHub Actions workflow, rather than built locally.

The app icon and thumbnail are real, rendered from the actual in-app SVG lettermark — not placeholders. Official CasaOS/ZimaOS App Store listing submission is pending; today it installs via CasaOS’s own custom-install-from-URL flow, pointed at the manifest above.

Developer

Architecture

A two-service Docker stack: a Next.js frontend and a NestJS backend, talking over an internal network you never expose.

Stack

  • Backend — NestJS 11 + Prisma 6 + PostgreSQL 16, session-based auth (Argon2id password hashing), REST API under /api/v1.
  • Frontend — Next.js 15 (App Router) + React 19 + TanStack Query + Tailwind CSS v4. Talks to the backend only through a same-origin server-side proxy (/api/* rewrites), so the session cookie stays first-party.
  • Monorepo — pnpm workspaces + Turborepo, Node.js 24.
  • Shared types — a packages/types workspace of TypeScript interfaces shared between the frontend and the backend API contracts.

Multi-tenancy

Every business record carries an immutable organizationId. Every repository query filters by the organization taken from the authenticated session — never from client input — so one instance can safely host more than one organization.

Authorization

A strict role hierarchy — EMPLOYEE < MANAGER < SALES_HEAD < COMPANY_ADMIN < MASTER_OWNER — enforced by a NestJS guard plus per-service checks (for example, an employee can only see their own attendance report).

File storage

Uploaded files (knowledge-base PDFs) live on local disk under UPLOADS_DIR — a named Docker volume in production — not as database blobs.

Developer

API Reference

The full REST surface under /api/v1, grouped by resource. Auth column: None = no session required, Session = any logged-in employee, Manager+ = Manager rank or above.

MethodPathAuthDescription
GET/api/health/liveNoneLiveness probe — always returns ok if the process is up
GET/api/health/readyNoneReadiness probe — fails if the database is unreachable
POST/api/v1/bootstrapNoneSelf-service organization creation — creates the company and its first Master Owner. Disabled when BOOTSTRAP_DISABLED=true
POST/api/v1/auth/sessionsNoneLog in with email + password; sets the httpOnly session cookie
POST/api/v1/auth/sessions/logoutSessionRevoke the current session and clear the cookie
POST/api/v1/auth/change-passwordSessionChange the authenticated employee's own password
GET/api/v1/employeesSessionList employees in the current organization
GET/api/v1/employees/meSessionGet the authenticated employee's own record
POST/api/v1/employeesManager+Create an employee — generates an employee number and a one-time temporary password
PATCH/api/v1/employees/:idManager+Update role, department, or employment status. Rank-guarded — cannot promote to the actor's own rank or above
POST/api/v1/employees/:id/reset-passwordManager+Force-generate a new temporary password for a subordinate and revoke their active sessions immediately
DELETE/api/v1/employees/:idManager+Separate an employee and revoke their sessions. Soft — history is preserved, the employee number is never reused
GET/api/v1/assignmentsSessionList work assignments, scoped to what the actor is allowed to see
POST/api/v1/assignmentsSessionCreate a new work assignment
POST/api/v1/assignments/:id/assignSessionAssign (or reassign) a piece of work to an employee by employee number
POST/api/v1/assignments/:id/transitionsSessionMove an assignment through the status pipeline. Invalid transitions are rejected server-side
GET/api/v1/work-sessions/meSessionThe authenticated employee's current shift/break state
GET/api/v1/work-sessions/report/:employeeIdSessionFull attendance report for one employee — login/logout times, worked and break minutes, per-session breakdown
POST/api/v1/work-sessions/startSessionStart a shift. Rejected if the employee already has an open session
POST/api/v1/work-sessions/:id/breaks/startSessionStart a break on an open shift
POST/api/v1/work-sessions/:id/breaks/:breakId/endSessionEnd the current break and return to working
POST/api/v1/work-sessions/:id/endSessionEnd the shift. Auto-closes a dangling open break first
GET/api/v1/knowledge/documentsSessionList knowledge-base documents visible to the actor
POST/api/v1/knowledge/documentsManager+Upload a PDF training document (starts as a draft)
POST/api/v1/knowledge/documents/:id/publishManager+Publish a draft document, making it visible to its assigned audience
POST/api/v1/knowledge/training-assignmentsManager+Assign a published document to a role or a specific person
GET/api/v1/knowledge/training-assignments/meSessionThe authenticated employee's assigned training and acknowledgement status
POST/api/v1/knowledge/training-assignments/:id/acknowledgeSessionAcknowledge a training assignment for its current published version
GET/api/v1/tips/feedSessionToday's tip feed for the employee front page
POST/api/v1/tipsManager+Create a new team tip
POST/api/v1/tips/:id/acknowledgeSessionMark a tip as seen/acknowledged
GET/api/v1/reports/dashboardSessionMaster dashboard data — headcount, assignments by status, overdue count, live who's-working board
GET/api/v1/reports/employees/:employeeIdSessionCombined attendance + assignment report for one employee
GET/api/v1/exports/employees.csvSessionExport the employee directory as CSV
GET/api/v1/exports/assignments.csvSessionExport work assignments as CSV
POST/api/v1/imports/employees/csvManager+Bulk-import employees from a CSV file, with row-level error reporting
GET/api/v1/integrations/google-sheets/statusSessionWhether the Google Sheets adapter is configured ({configured: false} if no credentials are set)
POST/api/v1/integrations/google-sheets/exportSessionPush data to the connected Google Sheet
POST/api/v1/integrations/google-sheets/importSessionPull data from the connected Google Sheet
Developer

Security Model

Session-based auth, server-side authorization, and an audit trail — no client-trusted state.

Password hashing

Passwords are hashed with Argon2id, the current OWASP-recommended algorithm for password storage.

Sessions

Sessions use random 256-bit tokens; only the SHA-256 hash of the token is stored server-side, never the raw value. Cookies are httpOnly and SameSite=lax with a 12-hour TTL, and are revoked on logout, on password change, and immediately when an employee is removed or has their password reset.

Audit trail

An audit_events table records who did what to what, and when — deliberately excluding plaintext temporary passwords and password hashes from its metadata, so the audit log itself can never leak a credential.

Authorization

Every privilege-sensitive action is re-checked server-side against the actor’s role and the target’s rank — a Manager literally cannot construct a request that promotes a peer, regardless of what the UI shows them. This is covered by the automated e2e test suite (see Local Development for how to run it).

No row-level security in Postgres

Tenant isolation (multi-organization separation) is enforced entirely in the application layer today, not via Postgres row-level security policies. Every query is scoped by organizationId taken from the session in application code.

Developer

Local Development

Run the frontend and backend directly with pnpm, against a local PostgreSQL instance, without Docker.

pnpm install

# Start a local Postgres however you like, then:
cd apps/backend
DATABASE_URL="postgresql://user:pass@localhost:5432/zulivio" npx prisma migrate dev
DATABASE_URL="postgresql://user:pass@localhost:5432/zulivio" pnpm dev   # backend on :4100

cd ../web
BACKEND_URL="http://localhost:4100" pnpm dev   # frontend on :3100

Running the test suite

cd apps/backend
pnpm typecheck   # tsc --noEmit
pnpm build       # nest build

# e2e/integration suite against a real Postgres (not mocked)
docker run --rm -d --name zulivio-test-pg -e POSTGRES_PASSWORD=test \
  -e POSTGRES_DB=zulivio_test -p 55432:5432 postgres:16-alpine
DATABASE_URL="postgresql://postgres:test@localhost:55432/zulivio_test" \
  npx prisma migrate deploy
DATABASE_URL="postgresql://postgres:test@localhost:55432/zulivio_test" \
  NODE_ENV=test npx jest --config ./test/jest-e2e.json --runInBand
docker rm -f zulivio-test-pg

The suite covers bootstrap/login/logout, privilege escalation blocked on both create and edit, cross-employee report access blocked, owner edit/reset-password/remove on subordinates, the employee directory scoped to strictly-below-your-rank at three rank levels (never a peer or higher), the audit log gated to the Master Owner, the full attendance state machine (including rejecting a second concurrent session/break), and the full assignment lifecycle (including rejecting invalid transitions and mutations on a terminal state). 41/41 passing at the last run.

Developer

Scope & Limitations

What's intentionally not built yet — stated plainly rather than silently faked.

This build implements the workforce-operations core of a much larger specification, not the full spec. Explicitly not built yet:

  • No leads/contacts/accounts/opportunities/pipeline CRM objects — this is an employee/assignment/attendance system, not (yet) a sales pipeline CRM.
  • No background job queue or Redis — CSV import and PDF upload run synchronously in the request. Fine at small-team scale; a very large CSV import or PDF library would need this added.
  • No S3-compatible object storage (MinIO) — files live on a local disk volume. Fine for a single-server deployment; not horizontally scalable as-is.
  • No automation/workflow-rule engine, and no AI features.
  • No MFA/SSO/OIDC — session + password only.
  • No WhatsApp/telephony/email/calendar adapters.
  • No row-level security (RLS) in Postgres — tenant isolation is enforced entirely in the application layer today.
  • Assignment/employee sequence numbers are computed via count() + 1 inside a transaction; the database’s unique constraint prevents a collision from corrupting data, but under heavy concurrent writes a request could need a retry rather than silently succeeding. Fine at normal team-scale write volume.

None of the above are silently faked — where a feature isn’t built, there is no button or endpoint pretending it works.

Developer

Roadmap & Contributing

What's shipped today is the foundation of a much larger, publicly-planned product.

Roadmap

What’s in the repository today is the workforce-operations foundation of a larger product: a shared identity/permission/relationship core with purpose-built departmental workspaces (Sales, Marketing, Service, Success, Delivery, Field Service, People, Partner/Vendor) layered on top over time — integrations-first rather than rebuilding accounting, payroll, or telephony in-house, and governed AI added only after the data foundation is solid.

The full multi-month, phase-by-phase implementation plan — department workspace designs, feature catalogue, migration/rollout playbook, and go/no-go checklists — lives in ROADMAP.md on GitHub, and is summarized on the homepage roadmap section.

Contributing

Zulivio is AGPL-3.0-only — see LICENSE. Bug reports and pull requests are welcome on GitHub:

Before opening a pull request, run the typecheck, build, and e2e suite described in Local Development — the same checks CI runs.