Skip to main content

Architecture

Parcelis uses a pnpm and Turborepo workspace with separate applications for the web interface, API, and background worker. Documentation is maintained in a separate repository.

Applications​

ApplicationResponsibility
apps/webNext.js App Router frontend with React and shared UI components.
apps/apiNext.js App Router API running in Node.js, with tRPC, REST, and Bull Board.
apps/workerRedis-backed background jobs and durable notification delivery.

The web app and API run as separate processes. Browsers use the web origin's /trpc paths; the development and deployment proxies forward those requests to the API. Background workers run independently of API request handlers.

Docker runtime images​

The modular stack uses parcelis/apps:latest for web and API processes managed by Supervisor. Nginx runs only in the separate parcelis/proxy image, which routes directly to app-service:3001 for web and app-service:4000 for API, tRPC, and the job dashboard. /healthz forwards to API health. Documentation routing follows the selected proxy configuration; when docs-service is present, /docs/ reaches that container.

The app binds both listeners to the container network, and the supplied modular Compose file publishes only the proxy HTTP port. The app health check requests both processes directly; the proxy checks routed web/API responses and waits for app health before startup. These are HTTP liveness and routing checks, not full dependency readiness.

API_INTERNAL_PORT defaults to 4000; custom modular builds must pass the same argument to app and proxy. Both application targets set API_INTERNAL_URL to the matching loopback API address so web middleware can validate organization access. Modular API requests now pass through one nginx hop. The supplied production environment and Compose configuration use API_TRUST_PROXY_HOPS=1; existing deployments must update a saved value of 2 for this path. Additional trusted proxies require a count matching the actual topology.

The optional single-image target retains bundled nginx on port 3000 and its own Supervisor configuration with loopback web/API listeners. The deployment guide explains paired app/proxy upgrades and rollback. Background processing remains in the existing parcelis/worker:latest image.

Dockerfile.app reuses its existing worker-build stage for an independent worker runtime target. It contains the compiled worker and shared database, email, and jobs packages with production dependencies. It excludes the web/API builds, docs, nginx, and Supervisor. Node runs directly so it receives shutdown signals; modular Compose allows 25 seconds before forced termination. The worker handles outbox dispatch, account-email delivery, scheduled lease changes, retries, and recovery. Existing Redis heartbeats and canary jobs report processing health; the image has no web HTTP health check.

The single-image target retains its bundled worker. Its application, migration, and worker containers all use parcelis/parcelis:latest; its worker container starts with pnpm --filter @parcelis/worker start. It does not require the dedicated worker image. Both deployment Compose files use latest for Parcelis application images and no longer read PARCELIS_VERSION.

API routes​

PathPurpose
/api/auth/*NextAuth email/password sign-in and session endpoints.
/trpc/*Typed application procedures through the tRPC Fetch adapter.
/api/v1/*REST endpoints documented in the API reference.
/api/v1/healthAPI health and public object-storage configuration.
/api/v1/readyDependency readiness; 503 when database, Redis, or storage fails.
/admin/jobs/*Administrator-only Bull Board dashboard and its assets.

NextAuth's Credentials provider validates email/password input, verifies Argon2 hashes, and enforces rate limits and account checks. NextAuth manages encrypted JWT cookies with a seven-day lifetime, renewal, and cross-tab sign-out. The API reads JWTs through NextAuth and checks the current user account and permissions. There is no database session table, idle timeout, or individual session revocation. Sign-out clears the browser cookie; copied JWTs remain valid until expiry unless the password changes or resets. Password changes update passwordChangedAt, and session checks reject JWTs issued before that timestamp. This also ends the current browser's session, so the user must sign in again. Rotating NEXTAUTH_SECRET invalidates all JWTs. Registration and account recovery remain API workflows.

The API resolves the authenticated session and active organization for each request. Operational reads and writes are scoped to that organization. Responses use private, no-store; dashboard requests validate the JWT and current account.

API-token management uses the session-only apiTokens.list, apiTokens.create, and apiTokens.revoke procedures in app.router.ts. Organization owners and administrators, plus platform administrators without organization membership, can manage tokens for the active organization context. The context resolves the selected organization when supplied, otherwise the user's default organization; if no default is set, it uses the first accessible organization. All token queries remain scoped to that resolved organization.

Creation returns the secret once and stores its SHA-256 hash and a display prefix containing pcls_api_ plus ten random characters. Expiration defaults to seven days. Creation is limited to ten attempts per user and organization per fifteen-minute window, per API process, using counters separate from sign-in. Lists include active, expired, and revoked records with cursor pagination. Revocation retains the record and preserves its original revoker and timestamp on repeated or concurrent calls. Token management requires a browser session.

The API tokens settings page is /o/{slug}/settings/organization/api-tokens; the unscoped route follows the selected/default organization. Token management and its navigation entry require an owner, organization administrator, or platform administrator. Other members can open the page but see an access message instead of token management controls. The table paginates token metadata and derives Active/Expired/Revoked status. Create token opens a Name/Expiration drawer with the shared seven-day default. Creation shows the credential once in a Copy/Done dialog; closing it, navigating, or switching organizations clears the temporary secret. It is never placed in query/mutation caches, persisted browser storage, URLs, or diagnostics. Requests capture their organization slug; organization-specific cache keys and request versions prevent late responses from appearing in another organization. Revoke confirms the token name, organization, and integration impact, retaining revoked rows. Rotate by creating a replacement, updating the integration, then revoking the old token.

REST /api/v1/* requests accept bearer credentials and resolve a user or token principal. Authorization takes precedence over cookies, including invalid headers. Invalid, revoked, or expired tokens return generic 401 responses with WWW-Authenticate: Bearer; database failures return sanitized 500 responses and never fall back to cookies. The scheme is case-insensitive and the secret is case-sensitive. Combined or duplicate credentials are rejected.

Tokens use their stored organization rather than their creator's identity or privileges. A conflicting organization-selection header returns 403. An explicit allowlist covers existing properties, units, tags, amenities, unit options, and tenant-image operations; a classification test requires new protected REST operations to be reviewed. Browser permissions, feature flags, supported actions, nested-record and upload ownership, and domain validation remain enforced. Foreign record IDs return 404. Account/platform administration and token management remain session-only; bearer tokens do not authenticate tRPC, NextAuth, or the jobs dashboard. A separately valid cookie retains its normal behavior there. Health and preflight remain public, with existing CORS origins unchanged.

Each protected request checks token validity in PostgreSQL without caching. Revocation stops new authentications after commit; already-authorized requests may finish. Last-used writes use an atomic five-minute condition and run independently of the business response. Failures emit a fixed diagnostic without credentials. Request handlers do not log Authorization headers. Activity writers attribute token requests to api-token:<id> / API token: <name> using request-local context; actor IDs remain strings.

The generated OpenAPI contract explicitly declares bearer security for reviewed organization REST operations and leaves health public. A contract test checks this against the token allowlist. The API authentication guide documents token creation, Bruno/curl setup, rotation, and troubleshooting.

The Node.js runtime supports Prisma database connections, Argon2 password hashing, S3-compatible image uploads, Redis queues, and invoice PDF generation. API startup and shutdown are managed by apps/api/scripts/server.mjs, which drains requests and closes initialized queue and database connections.

Shared packages​

  • @parcelis/schemas defines shared Zod validation and TypeScript contracts.
  • @parcelis/db owns Prisma models, migrations, and database access.
  • @parcelis/ui supplies reusable interface components and design tokens.
  • @parcelis/jobs defines queue names, payloads, and connection configuration.
  • @parcelis/email renders and delivers transactional email.
  • @parcelis/config supplies shared development configuration.

The API exports its router type for the frontend. Input validation belongs in @parcelis/schemas, and consistent multi-step writes use Prisma transactions.

Development and verification​

Start the local stack with pnpm dev. The launcher prints the selected URLs and ports. Run focused API checks with:

pnpm --filter @parcelis/api build
pnpm --filter @parcelis/api typecheck
pnpm --filter @parcelis/api lint
pnpm --filter @parcelis/api test

See End-to-End Testing for browser workflow checks and Deploy With Docker Compose for the deployment topology.

Documentation is maintained and deployed independently in parcelis/parcelis-docs. The application retains OpenAPI generation; Next.js redirects legacy /docs URLs to https://docs.parcelis.dev/.