Edgeweir
Project

Contributing

Development environment, checks, code conventions, and change procedures.

Ground rules

  • Open an issue before a large change.
  • Settle the design in an issue first for a new dependency, a change to the process or deployment model, the node channel protocol, NodeConfig IR semantics, or the security baseline.
  • Report security vulnerabilities privately as described in SECURITY.en.md, not in a public issue.
  • Pull requests that break the principles below are not merged.
PrincipleRequirement
Phone-home and licensingNo phone-home, no license-check code
TelemetryOff by default; third-party dependency telemetry hard-disabled
Sensitive dataPrivate keys, DNS API credentials, and similar secrets are envelope-encrypted with EDGEWEIR_MASTER_KEY before they reach the database; SSH credentials are never stored
Node identityThe node channel terminates its own TLS; enrollment tokens are single-use and stored as SHA-256 only; every node RPC after enrollment uses mTLS
API credentials/api/v1 accepts only x-api-key; /rpc accepts only the session cookie plus the CSRF header
AuditManagement actions write the audit log
Product boundaryCustomer portals, plans and billing, finance, and reselling do not enter core code behind license switches (LICENSING.en.md)
TestsChecks are never made to pass by skipping, deleting, or weakening tests
DependenciesDependency APIs and versions are verified against official documentation before use

Development environment

ToolVersionUsed for
Node.js24.11 or later (.nvmrc: 24)All development commands
pnpm12 (packageManager: pnpm@12.6.0)Workspace and scripts; corepack enable or npm i -g pnpm@12
Docker, Compose v2—Local PostgreSQL (compose.dev.yml), end-to-end tests
Go1.27.1helpers/certd, pnpm e2e
buf1.73.0Installed with the dev dependencies (@bufbuild/buf); called by pnpm lint and pnpm proto:*

End-to-end tests also need curl, jq, goreleaser v2, syft, cosign v3.1.3, a checkout of edgeweir-node next to this repository (or EDGEWEIR_NODE_CONTEXT) whose out/openresty holds the edgeweir-openresty packages for the Docker architecture (make openresty-packages; scripts/e2e.sh builds them first when missing, which needs Docker Buildx), a node image from before G3, edgeweir-node:pre-g3 (scripts/e2e-g3.mjs builds it from edgeweir-node commit 6da3403 when missing), and network access to deb.debian.org and openresty.org (building the node image or the edgeweir-openresty packages for the first time also needs github.com, download.gnome.org and vault.almalinux.org). The Playwright steps need Chromium:

pnpm --filter @edgeweir/console exec playwright install chromium

Local run

  1. Install dependencies.

    pnpm install
  2. Start local PostgreSQL 18. It listens on 127.0.0.1:5432 (change the port with DEV_POSTGRES_PORT); user, password, and database are all edgeweir.

    docker compose -f compose.dev.yml up -d
  3. Create .env and set EDGEWEIR_MASTER_KEY to the output of openssl rand, unchanged. DATABASE_URL in .env.example matches compose.dev.yml.

    cp .env.example .env
    openssl rand -base64 32
  4. Start the console.

    pnpm dev

    One process: :3000 serves the UI and API (the front end uses Vite HMR), :8443 serves the node channel. Server changes restart the whole process. Until setup completes, the startup log prints the setup token (setupToken field).

  5. Verify.

    curl -s http://localhost:3000/healthz

    Expected output: {"status":"ok","version":"dev"}. Open http://localhost:3000/setup and enter the setup token to complete the setup wizard.

Commands

CommandEffectWhen to run
pnpm lintBiome check and format verification, buf lint protoBefore every commit
pnpm formatBiome auto-fix (biome check --write .)When pnpm lint reports formatting
pnpm typecheckGenerates Paraglide messages and the route tree, then type-checks the whole workspaceBefore every commit
pnpm testVitest unit and integration tests; PostgreSQL is in-process PGlite, no DockerBefore every commit
pnpm buildProduction build: Vite front end and single-file serverWhen build configuration or dependencies change
pnpm proto:lintbuf lint protoWhen proto/ changes
pnpm proto:genGenerates TypeScript from proto/ into packages/protoWhen proto/ changes
pnpm db:generateGenerates a SQL migration from changes in packages/db/src/schema (drizzle-kit)When the schema changes
pnpm e2eEnd-to-end tests (scripts/e2e.sh); see End-to-end testsWhen the node channel, config compiler, install script, or UI flows change

CI runs on pull requests and pushes to master: pnpm lint, no diff in packages/proto after pnpm proto:gen, pnpm typecheck, pnpm test, pnpm build, go vet and go test -race for helpers/certd, the image build, and the end-to-end tests.

Tests

LevelToolLocation
Unit and integrationVitest (PGlite)apps/console/test/server, apps/console/test/web, packages/*/test
certdgo test -race ./...helpers/certd
UI flowsPlaywrightapps/console/e2e, driven by scripts/e2e.sh
End-to-endscripts/e2e.sh and scripts/e2e-*.mjscompose.e2e.yml

Run a single Vitest file from apps/console:

pnpm exec vitest run test/server/docs.test.ts

Tests enforce these conventions:

ConventionTest
Message keys and placeholders match; error codes, reason codes, and node error codes have messages; no hard-coded text in componentsapps/console/test/web/i18n.test.ts
No skeletons, no *Description components, no color literals, no links to other sites, appica-ui import scopeapps/console/test/web/ui-rules.test.ts
shadcn preset b2D0wqNxT; a single ThemeProviderapps/console/test/web/ui-preset.test.ts
A read-only AccessKey is refused by every write procedure and changes nothingapps/console/test/server/infrastructure.test.ts
Every procedure except system.status and system.setup requires credentialsapps/console/test/server/openapi.test.ts
.env.example lists every variable the console reads and compose interpolatesapps/console/test/server/env-example.test.ts
Third-party images pinned by digest, Actions by commit SHAapps/console/test/server/supply-chain-pins.test.ts
The compose templates embedded in deploy.sh match the repository files byte for byteapps/console/test/server/deploy-script.test.ts
Relative doc links resolve; the ARCHITECTURE.md data model lists every migration and tableapps/console/test/server/docs.test.ts
Migrations have contiguous numbers, increasing timestamps, and one SQL file plus one snapshot eachpackages/db/test/migrations.test.ts

Every new API procedure has a Vitest case.

End-to-end tests

  1. Build and start the test stack (postgres, console, node, test origins, and test clients).

    docker compose -f compose.e2e.yml up -d --build
  2. Run the tests. --up starts the stack first; --down removes the stack and its volumes afterwards; --skip-ui skips Playwright.

    pnpm e2e
  3. Verify: the output ends with E2E OK.

The variables for a second, parallel stack (COMPOSE_PROJECT_NAME, E2E_CONSOLE_PORT, E2E_NODE_PORT, E2E_TAG, E2E_SUBNET, E2E_ISOLATED_SUBNET, E2E_INSTALL_IMAGE, EDGEWEIR_NODE_CONTEXT) are listed in README end-to-end tests.

Commit conventions

Use Conventional Commits.

<type>(<scope>): <subject>

<body>

<footer>
typeUse
featNew feature
fixBug fix
docsDocumentation only
styleFormatting with no logic change
refactorRestructuring that is neither a feature nor a fix
perfPerformance
testNew or changed tests
buildBuild system, dependencies, Dockerfile
ciCI configuration
choreOther maintenance
revertReverts an earlier commit

Scopes: console, web, api, db, contract, compiler, proto, node-channel, certd, deploy, e2e, doc. Omit the scope for changes that span several modules.

Examples:

feat(deploy): add deploy.sh to install and upgrade 宝塔 / aaPanel compose deployments
fix(certd): renew without ARI replaces when the CA rejects the new order
feat(console)!: drop the public landing page from the open core
RuleRequirement
GranularityOne change per commit; each commit passes the checks on its own
SubjectChinese or English; at most 72 characters; no trailing period
Breaking changes! after the type or scope, and a BREAKING CHANGE: footer stating the impact and migration
DCOgit commit -s adds Signed-off-by, agreeing to the Developer Certificate of Origin; recommended

Code conventions

AreaConvention
TypeScript lint and formatBiome (configured at the repository root); no ESLint or Prettier
TypeScript typesstrict; avoid any; zod validates system boundaries (API input, environment variables, external data)
Go (helpers/certd)gofmt; passes go vet

UI rules

AreaRule
Componentsshadcn components live in components/ui and use the Base UI render prop, not asChild; one ThemeProvider for the app
StatesEvery page has loading, empty, and error states
CopyShort and user-facing; no page subtitles; no description paragraphs in dialogs or cards; empty states are a title plus an action; only one-line safety notes (e.g., "Shown once"), rendered with SafetyNote
LoadingNo skeletons or animate-pulse; TopProgress (2px top bar) covers route loads, fetches, and mutations; first loads render LoadingState; submit buttons show Spinner and are disabled; polling queries set meta: { background: true }
ColorsNo color literals in TS/TSX; colors come from CSS tokens
External linksNo links to other sites, except the allow list in ui-rules.test.ts
appica-uiOnly through src/web/components/appica/ and appica-bridge.css (scoped tokens, one @source per component)
MotionEntrances use animate-enter with a staggered animationDelay; reduced motion is respected
Page scopeNo marketing pages in the open core

Internationalization

  • Every UI string goes through a Paraglide message function. Message files: apps/console/messages/zh-CN.json (default locale) and apps/console/messages/en.json.
  • Keys are snake_case and start with the page or feature, e.g., nav_sites, cert_brotli_unavailable.
  • Both locales change together: same key set, same placeholders, no empty values.
  • English counts use plural variants of the inlang message format (local countPlural = count: plural, one match entry per plural category, every variant with the same placeholders); zh-CN keeps a plain string.
  • src/web/routes and src/web/components contain no Chinese literals and no English UI text, including aria-*, title, alt, and placeholder.
  • The server does not build user-facing sentences: the API returns error codes and the UI translates them; unknown codes fall back to the server's English message.

Error codes

  1. Add the code to errorDefs in packages/contract/src/errors.ts with its HTTP status and params.
  2. On the server, call fail(CODE, message, data) (apps/console/src/server/lib/errors.ts): message is the English fallback, data supplies the fields named in params.
  3. Add the message to both message files, with placeholders matching params.
Code tableLocationMessage key
API error codes errorDefspackages/contract/src/errors.tserror_<lowercase code>
Revision reasons revisionReasonDefspackages/contract/src/errors.tsrevision_reason_<code>
Node error codes nodeErrorDefspackages/contract/src/node-errors.tsnode_error_<code>
Node task outcomes taskErrorDefspackages/contract/src/node-errors.tstask_error_<code>
Prefetch failure reasons prefetchFailureReasonDefspackages/contract/src/node-errors.tstask_error_reason_<code>

API and server conventions

ConventionLocation
API changes start in the oRPC contract; the same procedure serves /rpc (UI) and /api/v1 (OpenAPI)packages/contract
Every procedure except system.status and system.setup uses the authed guard (session, AccessKey, or service account key)apps/console/src/server/rpc/base.ts
Service accounts call only the procedures serviceAccountProcedures lists and their scopes allow; a new procedure is closed to them by defaultpackages/contract/src/service-accounts.ts
Management actions call recordAudit inside the change's transaction to write audit_logapps/console/src/server/services/audit.ts
better-auth's own endpoints are audited by hooksapps/console/src/server/lib/auth-audit.ts
better-auth HTTP endpoints are allow-listed (AUTH_HTTP_ROUTES); every other path under /api/auth returns 404; the admin plugin is called server side only (auth.api.*, by the setup wizard)apps/console/src/server/lib/auth.ts
Client IPs come only from resolveClientIp: the TCP peer; forwarding headers only when the peer is in EDGEWEIR_TRUSTED_PROXIESapps/console/src/server/lib/client-ip.ts
Envelopes are bound to their row: masterKey.seal(value, { purpose: "<table>.<column>", recordId })apps/console/src/server/lib/envelope.ts
Special-purpose address ranges for origins (the node keeps the same list)packages/contract/src/addresses.ts

Proto changes

proto/ is the only contract between the console and edgeweir-node.

  1. Edit the .proto files under proto/. Within the edgeweir.node.v1 package, only backward-compatible additions; a breaking change needs a new v2 package. NodeConfig follows canonical-encoding constraints: no map fields, fields declared in ascending field-number order.

  2. Run the linter.

    pnpm proto:lint
  3. Compare against the previous proto tag to confirm there is no breaking change.

    pnpm exec buf breaking proto --against '.git#tag=proto/vX.Y.Z,subdir=proto'
  4. Generate TypeScript and commit the proto/ change together with the generated code in packages/proto. CI checks that the generated code matches proto/.

    pnpm proto:gen
  5. After the merge to master, a maintainer tags proto/vX.Y.Z: new fields or RPCs bump the minor version, comment-only changes bump the patch version.

  6. In edgeweir-node, set PROTO_TAG in the Makefile to the new tag, regenerate the Go code, adapt the node, and commit internal/gen/.

    make proto
  7. Release order: upgrade the console first, then the nodes. The console stays compatible with nodes on the previous proto version.

Database schema changes

  1. Edit packages/db/src/schema.

  2. Generate the migration.

    pnpm db:generate
  3. Commit the new SQL file and meta/ snapshot in packages/db/migrations together with the schema change.

  4. List the new migration file name and any new table names in the data model section of ARCHITECTURE.md.

  5. Verify.

    pnpm test

The console runs database migrations at startup.

Environment variables

  1. Declare the variable in the schema in apps/console/src/server/lib/env.ts (zod validation and default).

  2. Add it to .env.example with a comment stating its purpose and default. Variables interpolated by the compose files also go into .env.example.

  3. Describe the variable in both language versions of the environment variables reference.

  4. Verify.

    pnpm test

Operator configuration belongs in System; environment variables keep only values needed before setup or at the infrastructure level. When a setting exists both in system settings and as an environment variable, precedence is: value saved in System, environment variable, default.

Updating pinned images and actions

Third-party inputs are pinned by immutable references; pnpm test rejects unpinned references, and edgeweir-node runs the same check with make pin-check. Images built from this repository or edgeweir-node are referenced by tag.

InputFormLocation
Third-party imagestag@sha256:<multi-arch index digest>Dockerfile (# syntax= line and ARG *_IMAGE), compose*.yml, scripts/e2e.sh (E2E_INSTALL_IMAGE default), deploy.sh (PG_IMAGE and embedded templates)
GitHub Actionsowner/action@<40-character commit SHA> # vX.Y.Z.github/workflows/*.yml
  1. Look up the image digest. The Digest in the output is the multi-arch index digest; do not use a single-platform digest.

    docker buildx imagetools inspect <image>:<tag>
  2. Look up the commit for an action release. For an annotated tag, use the SHA on the line ending in ^{}.

    git ls-remote --tags https://github.com/<owner>/<action>
  3. Change the tag and digest (or the SHA and version comment) together.

  4. After changing compose.baota.yml or compose.baota-host.yml, copy the full file into the embedded template in deploy.sh; keep PG_IMAGE in deploy.sh equal to the PostgreSQL image in compose.baota.yml.

  5. Verify.

    pnpm test
    bash deploy.sh template bundled | diff - compose.baota.yml
    bash deploy.sh template host | diff - compose.baota-host.yml

After a base image update, run the Release workflow manually in Actions to rebuild the tip of master.

Releases

ArtifactTriggerVersion
Console image ghcr.io/marvinli001/edgeweirThe Release workflow publishes each master commit whose CI passed; a manual Release run rebuilds only the tip of master<YYYYMMDD>-<first 7 characters of the commit> (UTC commit date, scripts/image-version.sh); latest moves along while that commit is still the tip of master
Nodev* tags in edgeweir-nodevX.Y.Z
ProtoTag set by a maintainer on masterproto/vX.Y.Z

Version pinning, upgrades, and rollback: Versions, upgrades, and rollback. Artifact signatures and verification: SECURITY.en.md.

Documentation

  • Published docs are plain GitHub Markdown: name.md (Simplified Chinese, default) and name.en.md (English, same sections as name.md).
  • The documentation site lives in doc/ (Fumadocs, Next.js static export) as its own pnpm workspace (doc/pnpm-workspace.yaml, doc/pnpm-lock.yaml). .github/workflows/docs.yml builds the site on pull requests and publishes it to GitHub Pages on pushes to master: https://marvinli001.github.io/edgeweir/.
  • doc/scripts/sync-content.mjs holds the page map (SECTIONS) and converts the Markdown. A new page must be added to the page map.
MarkdownSite
H1Page title
One-line paragraph right after the H1Page description
GitHub alerts (> [!NOTE], etc.)Callouts
Relative links to published pagesSite URLs
Relative links to other repository filesGitHub URLs
Broken relative links, raw HTMLCI build failure
CommandEffect
pnpm --dir doc installInstalls the site dependencies
pnpm --dir doc devLocal site on :3000
DOCS_BASE_PATH=/edgeweir pnpm --dir doc buildStatic export to doc/out
DOCS_BASE_PATH=/edgeweir pnpm --dir doc previewServes doc/out on :4100 under /edgeweir

Pull request checklist

  • One topic per pull request; the description states the motivation, the changes, and how they were verified, and links the issue.
  • pnpm lint, pnpm typecheck, and pnpm test pass; pnpm build and pnpm e2e run as listed in Commands.
  • Commits follow the commit conventions.
  • UI copy is updated in both zh-CN and en; new error codes have messages.
  • Schema changes include the migration SQL, the snapshot, and the ARCHITECTURE.md data model update.
  • New environment variables are in env.ts, .env.example, and the environment variables reference.
  • Management actions write the audit log.
  • UI changes include screenshots.
  • Behavior changes are reflected in the Chinese and English docs (README, deployment docs).
  • No test was skipped, deleted, or weakened.

License

Edgeweir is released under AGPL-3.0-only. Submitting a contribution licenses it under AGPL-3.0-only and confirms the right to do so.

The open core permits compliant commercial use. The open-source edition serves a single operator; multi-tenancy (organizations, members, roles, and isolation), the customer-facing portal, plans and billing, finance, and reselling belong to a separate commercial operations product (LICENSING.en.md). Contributing to the core does not automatically grant the project a right to relicense under proprietary terms; a dual license or a plugin linking exception requires separate verification of code ownership and contributor authorization.

Edit on GitHub

On this page