Write once, prove when, publish everywhere. An Astro SSR app that runs a post through a timestamp → delay → translate → crosspost pipeline: an OpenTimestamps proof, a holding delay, machine translation into seven locales, and pluggable delivery to Bluesky, GitHub, Mastodon, Threads, and more.
post → OTS stamp → delay (~5h, verify OTS) → translate (Google, 7 locales)
→ update this SSR site → Bluesky (en) → GitHub deploy (all langs)
→ other providers (pdb/en, yay/ja, threads/en, mastodon/en, …)
| Dashboard | Published post |
|---|---|
![]() |
![]() |
The pipeline status per post, and a published post with its locale switcher.
These images are generated from a throwaway demo database — see
docs/demo-seed.mjs and docs/shot.cjs.
- Astro 5 (SSR,
@astrojs/nodestandalone adapter) - SQLite + Prisma 6
- OpenTimestamps (npm
opentimestamps, in-process — no Python client needed) - Google Cloud Translation v2 (REST + API key)
- Providers: Bluesky (
@atproto/api), GitHub (octokit), Mastodon/Threads (REST), pdb/yay (cookie auth)
- Node ≥ 20.10 (
@atproto/apiusesimport … with { type: 'json' }). This repo pins Node 22 via.nvmrc— runnvm use.
nvm use # Node 22
npm install
cp .env.example .env # then fill in secrets; use an ABSOLUTE DATABASE_URL path
npm run setup # prisma generate + db push + seed the single user
npm run build && npm startDev mode: npm run dev.
| Route | Access | Purpose |
|---|---|---|
/login |
public | single-user email/password sign-in |
/{locale} |
public | published timeline for that locale |
/{locale}/p/{postid} |
public | a published post (with locale switcher) |
/ |
admin | dashboard: pipeline status per post |
/post |
admin | create a post (enters the pipeline) |
/config |
admin | edit settings (overlay on .env) |
Auth is a SameSite=Lax httpOnly session cookie; that also provides CSRF
protection (Astro's origin check is disabled — see astro.config.mjs).
Statuses: timestamping → awaiting_verification → translating → publishing → published (failed on error). A background scheduler (src/lib/scheduler.ts,
default 60s) advances in-flight posts; advancePost() does one transition at a
time and is safe to call repeatedly.
- Delay (
DELAY_HOURS, default 5) is the primary gate before translating. - OTS is verified best-effort during the wait; set
otsRequireVerifiedto make a confirmed attestation a hard gate (may exceed the delay — Bitcoin is slow).
Add a destination by implementing Provider (src/lib/providers/types.ts) and
registering it in src/lib/providers/index.ts. Each provider declares which
locales it posts (['en'], ['ja'], or ['all'] for GitHub). Password/token
providers read .env; cookie providers read
./data/credentials/<name>.json.
tests/e2e.mjs drives a running server with Playwright (login → create →
watch the pipeline reach published → check the public pages).
BASE_URL=http://localhost:PORT npm run test:e2eIssues and PRs welcome — see CONTRIBUTING.md and the Code of Conduct. For vulnerabilities, follow SECURITY.md (please don't open a public issue).
This project is dual-licensed:
- The application as a whole is licensed under the GNU AGPL-3.0-or-later. If you run a modified version as a network service, you must offer users its source.
- The reusable library layer under
src/lib/— the pipeline, the OpenTimestamps helper, translation, config, and the provider adapters — is licensed under the permissive MIT license, so you can lift those modules into your own projects freely.
Each source file carries an SPDX-License-Identifier header stating which
license applies.


