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
| Application | Responsibility |
|---|---|
apps/web | Next.js App Router frontend with React and shared UI components. |
apps/api | Next.js App Router API running in Node.js, with tRPC, REST, and Bull Board. |
apps/worker | Redis-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
| Path | Purpose |
|---|---|
/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/health | API health and public object-storage configuration. |
/api/v1/ready | Dependency 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/schemasdefines shared Zod validation and TypeScript contracts.@parcelis/dbowns Prisma models, migrations, and database access.@parcelis/uisupplies reusable interface components and design tokens.@parcelis/jobsdefines queue names, payloads, and connection configuration.@parcelis/emailrenders and delivers transactional email.@parcelis/configsupplies 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/.