Back to the deck
A card for non-profit work

For non-profits

Next.js, Payload CMS, Cloudflare

Websites and staff tools for the organisations that run on volunteers.

LYPM多倫多靈巖淨院

A bilingual website and staff office for a Toronto monastery, replacing Wix.

Next.js 16, Payload CMS 3, Cloudflare Workers, D1, R2

RehearsalRuns on synthetic data and sink mail. Wix and the live domain stay untouched until the monastery approves activation.

Two apps that talk to each other. The public site is Next.js on Cloudflare Workers; the staff workspace is Payload, styled in the monastery’s ivory, maroon and gold, with bilingual collection names and an office dashboard instead of a generic admin. Staff post services and news reports themselves: drafts, review, approval, and scheduled publishing.

Behind the admin is the real office work. Applications arrive privately, receipt is never mistaken for a seat, and a person confirms each one. A single atomic transition checks capacity, decides confirmed or waitlisted, writes the audit record and queues the notice, so two people can never take the last seat. Cancelling frees a seat for the waitlist.

  • 114 bilingual routes checked live
  • 302 images checked live
  • 180 automated CMS tests
  • 0 console errors or known vulnerabilities
Imported, not retyped
23 service and course pairs, 6 news pairs, 8 background pages, 5 slides, 9 intake forms, 8 PDF resources and 79 media files. The importer re-runs safely and never overwrites a staff edit.
Two languages, properly
Chinese and English are explicit pairs, each with its own ready flag. If only Chinese is ready, English shows a labelled Chinese fallback instead of a blank page. Old numeric URLs keep working.
Four staff roles
Administrator, publisher, editor and office. An editor can prepare a draft but cannot publish or open private applications.
Lunar dates
Calculated offline for the Chinese Buddhist calendar and checked against the Hong Kong Observatory for every day of 2025–2027. Manual wording is preserved and recalculation is explicit.
Event images
An ordered list per event with per-language visibility, captions as alt text, drag to reorder, and rotation that saves a new file and keeps the original.
Private intake
Nine forms: contact, volunteering, chanting support, seven-day dedication, refuge, pet refuge, precepts and private prayer. There is deliberately no public prayer wall.
Newsletter consent
Signup records consent but sends nothing until the single-use confirmation link is used. Unsubscribe and provider suppression are honoured, and a campaign needs a publisher, approved content and a finished subscriber sync.
Visitor protection
Turnstile and rate limits on intake, newsletter and member login in production. The staff workspace sits behind Cloudflare Access, draft previews are scoped and expire, and guest management links are hashed.
Reliable delivery
A durable outbox, polled every minute by a Durable Object alarm, sends mail and invalidates the site cache. Jobs are leased and retried up to eight times, and failures surface on the dashboard. Delivery is at least once, and that is documented rather than hidden.
Backups
Encrypted Restic backups kept outside git, with a restore check that confirms database integrity and that every media object is present.
Checked, not assumed
87 integration, 48 security, 14 backend workflow and 31 browser tests on the CMS, plus 14 on the website and 18 live end-to-end scenarios against the deployed Workers.

LYMT Canada加拿大靈巖山寺

An audit and exit plan for a WordPress site that grew into an application.

Audit, migration plan, Payload CMS, Postgres

Nearly doneThe new site is built and running at lymt.devin.tw. A few features are still being ported.

The site is half WordPress and half a custom plugin: donations and orders, class sign-ins, live streams, e-books, members and well over a hundred registration forms, on top of roughly 900 posts and 9 GB of media. I read it over a read-only session and inventoried what it actually does before proposing anything.

The plan moves it onto the same Payload and Next.js foundation in phases: a full backup first, then the spec, the foundation, a scripted and re-runnable content migration with a redirect for every old URL, the app features with donations built first because they carry the most risk, and a cutover that keeps WordPress read-only for three months.

Visit lymt.devin.tw ↗

Phase one fixes
One stops a global session start from defeating the edge cache on every public page. The other stops a dropdown from covering the sign-up form and brings the palette back to the temple’s maroon.
Questions first
Who posts and in which languages, which payment gateway is live, how tax receipts are issued, and who owns the hosting and payment accounts. These go to the temple before any code.
Findings
Security findings from the audit are deliberately not published here.
How it is run
Payload CMS
Staff own the content. Collections, roles, approval flows and audit records replace plugins and shortcodes.
Modern Next.js
App Router on Cloudflare Workers through OpenNext. Pages are cached at the edge and a publish invalidates exactly what it should.
Infisical and SOPS
Secrets are injected into the process, never kept in a repository or a file. Build artifacts are scanned for private values before every deploy.
Plane
Issues and decisions tracked in one place the whole team can read.
Git hygiene
Secret-shaped files are ignored, a gitleaks hook runs before every commit, files are staged by name, and commits follow the conventional format. CI type-checks, lints, audits, tests and dry-runs the Worker for each app.
Rehearse, then activate
Separate rehearsal and production resources. Activation is a written procedure the organisation approves, not a side effect of a deploy.
Privacy by default
Tests and checks run on synthetic data. No private office records, subscriber lists or member data are read to verify the work.