Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ PORT=3000
HOST=127.0.0.1
PUBLIC_ORIGIN=http://localhost:3000
ALLOWED_HOSTS=localhost,127.0.0.1
# Production registry storage. When set, PostgreSQL is used and W3BS_DATA_DIR is ignored.
# Railway: reference the Postgres service, e.g. ${{Postgres.DATABASE_URL}}.
DATABASE_URL=
# Local development and tests fall back to SQLite in this directory.
W3BS_DATA_DIR=.local/data
# Set a random secret of at least 32 characters to enable publishing/revocation.
# Keep this in your deployment secret manager, never in the repository.
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ permissions:
jobs:
validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_PASSWORD: w3bs
POSTGRES_DB: w3bs
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# Registry tests run against SQLite and, with this set, PostgreSQL as well.
W3BS_TEST_DATABASE_URL: postgres://postgres:w3bs@127.0.0.1:5432/w3bs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ W3BS_API=http://localhost:3000 node native/client.mjs w3bs://prompt/w3bs/researc

- Three implemented protocol drafts and five initial proposals, governance and contribution process.
- Strict URI and manifest validation, RFC 8785 canonicalization, Ed25519 signatures and namespace-bound pinned public keys.
- Persistent SQLite registry with ten signed prompt examples, immutable publication and resource revocation.
- Persistent registry with ten signed prompt examples, immutable publication and resource revocation. PostgreSQL in production (`DATABASE_URL`), SQLite for local development and tests.
- Shared search, resolve, inspect, verify, publish, run, revoke and conformance operations over CLI, HTTP and MCP, including stdio and Streamable HTTP.
- Responsive web inspector, prompt search, manifest downloads, explicit template-rendering consent and a PWA offline state.
- An experimental native URI handler with an independent parser and signature verifier.
Expand All @@ -46,9 +46,9 @@ Conformance compares the actual API, CLI process, HTML inspection envelope, HTTP

## Configuration and operation

See `.env.example`, `docs/developers.md` and `docs/deployment.md`. Production requires a persistent `/data` volume, an explicit public origin and host allowlist. Administrative writes are disabled without a publisher token. Example private signing material stays in ignored `.local/keys` and is not required by the server.
See `.env.example`, `docs/developers.md` and `docs/deployment.md`. Production requires a PostgreSQL `DATABASE_URL`, an explicit public origin and host allowlist. Administrative writes are disabled without a publisher token. Example private signing material stays in ignored `.local/keys` and is not required by the server.

The Docker image runs one Node service as a non-root user. Use a single replica with SQLite. Source, governance proposals, issue templates and conformance fixtures are intended for public release.
The Docker image runs one Node service as a non-root user against the self-hosted PostgreSQL service in the same Railway project. Source, governance proposals, issue templates and conformance fixtures are intended for public release.

## Layout

Expand Down
10 changes: 5 additions & 5 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ The service is deployment-ready source. Production launch requires publishing th
| `PORT` | Supplied by the host |
| `PUBLIC_ORIGIN` | `https://w3bs.org` |
| `ALLOWED_HOSTS` | `w3bs.org,www.w3bs.org,specs.w3bs.org,browse.w3bs.org,prompt.w3bs.org` plus the exact generated preview hostname if used |
| `W3BS_DATA_DIR` | `/data` on a persistent volume |
| `RAILWAY_RUN_UID` | `0` for Railway's root-owned volume; the bootstrap drops to uid/gid 1000 before starting HTTP |
| `DATABASE_URL` | PostgreSQL connection string. On Railway, `${{Postgres.DATABASE_URL}}` from the self-hosted Postgres service |
| `W3BS_DATA_DIR` | Only used without `DATABASE_URL`: SQLite directory for local development |
| `W3BS_PUBLISH_TOKEN` | Random administrative secret, at least 32 characters; absent means read-only |
| `W3BS_TRUST_FILE` | Optional path to a mounted operator-managed trust file; otherwise bundled example public keys |

Never deploy `.local/keys` or upload publisher private keys. The container needs public keys and pre-signed examples only. Store any administrative token in the provider's secret manager. Back up the SQLite database and trust configuration. Use one replica; horizontal replication requires replacing the local SQLite adapter.
Never deploy `.local/keys` or upload publisher private keys. The container needs public keys and pre-signed examples only. Store any administrative token in the provider's secret manager. Back up the PostgreSQL database and trust configuration. The schema is created on first start; the ten bundled examples are seeded once and never re-inserted.

## Railway

Create a service from the reviewed repository or local Docker context. The repository includes `Dockerfile` and `railway.json`. Attach a persistent volume at `/data`, provide the settings above and deploy. Health is `/healthz`.
The production project is `w3bs` in the Profullstack workspace: a self-hosted PostgreSQL service (Railway's `postgres-ssl` image, one replica, its own volume) and the `w3bs` web service built from this repository's `Dockerfile` on every push to `main`. `railway.json` sets the health check to `/healthz`, which reports `"storage": "postgres"` when the database is in use.

The health endpoint accepts Railway's `healthcheck.railway.app` hostname; that exception does not permit access to other routes. Railway volumes mount as root, so the container bootstrap initializes only the data directory and registry database files, then drops privileges before importing the server. It also works when launched directly as the image's default `node` user with an already writable volume. Provider references: https://docs.railway.com/deployments/healthchecks and https://docs.railway.com/volumes
The health endpoint accepts Railway's `healthcheck.railway.app` hostname; that exception does not permit access to other routes. No application volume is needed. If a database URL carries `sslmode=require` (Railway's public proxy uses a self-signed certificate), the connection uses TLS without certificate verification; the internal URL has no `sslmode` and runs in plain TCP inside the private network. Provider references: https://docs.railway.com/deployments/healthchecks and https://docs.railway.com/guides/postgresql

Add `w3bs.org`, `specs.w3bs.org`, `browse.w3bs.org` and `prompt.w3bs.org` as custom service domains. Read each exact DNS target and verification record from the hosting provider; do not guess them. Update the registrar records, replacing the parking records only for those requested website names. Wait for domain validation and certificate issuance.

Expand Down
4 changes: 2 additions & 2 deletions fixtures/conformance-report.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"status": "passed",
"testedAt": "2026-09-14T18:11:16.415Z",
"testedAt": "2026-09-16T17:31:35.889Z",
"implementationVersion": "0.1.0",
"sourceFingerprint": "05135f148e24f47f458afe99be87690460c0f32888299fed6a16b6e6486efbbe",
"sourceFingerprint": "5ef70588c02e655abee500bd3de87b75f3085e914fd02d2705143818d96558c7",
"nodeVersion": "v24.18.1",
"environment": "isolated loopback registry, not production",
"resource": "w3bs://prompt/w3bs/research@1",
Expand Down
147 changes: 147 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"canonicalize": "2.1.0",
"express": "5.1.0",
"marked": "16.4.2",
"pg": "^8.23.0",
"zod": "4.3.6"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/conformance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function sourceFingerprint() {
}
export async function conformance() {
const instance = await startServer({
store: openStore({ path: ':memory:' }),
store: await openStore({ path: ':memory:' }),
port: 0,
host: '127.0.0.1',
});
Expand Down
6 changes: 3 additions & 3 deletions src/operations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function authorize(token, expected = process.env.W3BS_PUBLISH_TOKEN) {
if (actual.length !== target.length || !timingSafeEqual(actual, target))
throw new W3bsError('UNAUTHORIZED', 'A valid registry publisher token is required.', 401);
}
export function dispatch(store, operation, args, { token, publishToken } = {}) {
export async function dispatch(store, operation, args, { token, publishToken } = {}) {
const definition = operations[operation];
if (!definition) throw new W3bsError('UNKNOWN_OPERATION', 'Unknown W3BS operation.', 404);
const parsed = definition.schema.safeParse(args);
Expand All @@ -74,10 +74,10 @@ export function dispatch(store, operation, args, { token, publishToken } = {}) {
);
if (definition.write) authorize(token, publishToken);
const input = parsed.data;
if (operation === 'search') return { resources: store.search(input.query, input.type) };
if (operation === 'search') return { resources: await store.search(input.query, input.type) };
if (operation === 'publish') return store.publish(input.manifest);
if (operation === 'revoke') return store.revoke(input.uri, input.reason);
const result = store.inspect(input.uri);
const result = await store.inspect(input.uri);
if (operation === 'verify')
return { canonicalUri: result.canonicalUri, verification: result.verification };
if (operation === 'run') {
Expand Down
Loading
Loading