I began rebuilding MariaValenciaTrumpet.com as a website migration. The scope changed when lesson booking, MariaFiles and live video had to share accounts, permissions and operational rules. What had looked like a new public site became a working platform for Maria’s students. The project credits record my role in its design and development.
The public pages still cover biography, events, courses and contact. Signed-in students can also book and pay for lessons, open private teaching material, join one-to-one video rooms and receive recordings. Building those features forced me to decide which checks belonged in the browser, which belonged on the server and which had to survive a provider retry or an interrupted request.
When the migration became platform work
I kept the public site on Astro and deployed it through Cloudflare Workers. This gives the editorial pages direct HTML and localized content without turning the entire site into a client-side application. JavaScript is added where a form, media room or account flow actually needs it.
MariaFiles changed the shape of the project. The old members area came from a WordPress plugin, but its replacement needed authenticated server rendering and server-side access control. I split its data according to what the application does with it:
- Cloudflare D1 stores users, roles, lesson schedules, orders, permissions and audit records.
- Cloudflare R2 stores private PDFs, audio, video, lesson attachments and recordings.
- Worker sessions and short-lived state keep authentication and temporary coordination away from browser storage.
The one-to-one room uses Cloudflare RealtimeKit for media transport behind a custom interface. Stripe provides hosted checkout, Resend delivers transactional email, and a separate Worker runs the Telegram operations bot. I kept that bot outside the website runtime so its token and internal actions would have a smaller surface.
Booking needed more than a calendar
The first calendar prototype made availability look like a visual problem. It became a state problem as soon as lessons, packages, coupons and payments were connected. A slot may be open, held temporarily, paid, cancelled, released or forfeited. A zero-value coupon must create the right entitlement without calling a payment provider.
I modelled the order, payment attempt, lesson and credit ledger as related records. Provider webhooks are processed idempotently so the same event cannot grant a credit twice. Before confirming a booking, the server checks the slot again; the green cell a student selected a few seconds earlier is only a snapshot.
One scheduling bug made this clear. The interface checked a lesson’s start time, while the schedule needed to validate its end time too. Near a blocked period, the calendar could offer a slot that the full lesson would overrun. Availability now covers the complete interval in the teaching time zone and stores the result in UTC.
Then eight missing bytes broke MariaFiles
For the members-area migration, I preserved the concepts students already knew: files, categories, roles and personal access. I did not preserve the old PHP plugin’s security model. Catalog queries are filtered on the server, and preview or download routes recheck both the session and the access-control list. Private downloads use short-lived signatures tied to the user.
The first import appeared healthy in the database, but PDFs opened as grey pages and audio or video previews failed. The source was a WordPress .wpress backup, which is neither a ZIP nor a TAR archive. My first extraction pass had removed eight bytes from the beginning of each media payload.
I extracted the files again from the correct offset, hashed them, compared their sizes with the archive records and checked their real signatures: %PDF, JPEG markers, ID3, MP4 ftyp and the other expected headers. Only then did I update R2 and D1. Since that incident, a matching filename, MIME label and database row are never enough to approve an imported binary.
Real lessons rewrote the media-room checklist
I built the lesson room on RealtimeKit’s core SDK and kept control of the camera, microphone, screen sharing, chat and recording interface. The website creates access only after checking the signed-in user, lesson ownership, role and permitted time window. A copied room URL cannot provide those checks by itself.
Desktop testing covered the basic flow but missed the failures students would actually meet. Sessions on Windows and Android exposed different audio-routing behavior. Older Android hardware needed more time when switching cameras. On narrow screens, the chat panel competed with the video stage even though the same layout looked comfortable on a desktop monitor.
Those sessions led to a pre-join audio test, conservative media fallbacks, a speaker selector only on browsers that support it, delayed camera reacquisition and a chat panel separated from the stage layout. I still treat physical-device sessions as part of development because browser emulation cannot reproduce every WebRTC or audio-output path.
Recording added a separate consent flow. The server refuses to start when the adult/minor status or the required authorization is unknown. Consent is recorded for the lesson and recording event instead of being stretched across future sessions. Declining does not prevent the lesson from taking place; revocation triggers an attempt to stop an active recording. Completed recordings are transferred into private R2 storage and receive the student’s MariaFiles access rules.
Small failures changed the release process
Some of the longest investigations began with details that looked unrelated to the main application. The cookie panel, for example, worked during ordinary testing but disappeared for some desktop users. Cache behavior was one cause. Content blockers reacted to recognizable class names, automatic focus caught a leftover Enter key, and some extensions dispatched synthetic clicks.
The panel is now present before JavaScript runs, does not take initial focus, versions its stored choice and accepts the first decision only from a trusted browser event after a short stabilization period. Analytics remains denied until that decision.
Email clients produced a different set of surprises. SVG and ICO marks that rendered correctly on the website were unreliable in messages, so the transactional templates use a dedicated PNG asset. Private lesson links also needed different handling from public marketing URLs.
A deployment taught me to inspect one more layer. The Worker returned healthy HTML while a referenced CSS asset was missing. Release smoke tests now download the live page and request the exact CSS and JavaScript files found in that HTML. A successful deployment command is no longer enough evidence for me to close a release.
The decisive checks stayed on the server
Every private endpoint validates the request on the server. State-changing requests check origin signals; JSON and multipart bodies have explicit size limits; uploads use extension, MIME and file-signature allowlists. Persistent rate limits cover login, uploads, password resets and bulk actions.
Session tokens are opaque and stored as hashes. Their cookies use HttpOnly and SameSite, while administrative routes verify the role on every request. Private responses carry no-store and noindex, but access still depends on authentication and authorization. R2 buckets remain private, remote retrieval passes an SSRF guard, and audit events avoid raw IP addresses, credentials, provider tokens and object keys.
The same rule applies to payments. Prices and entitlements are calculated server-side, repeated payment events are safe to replay, and hosted checkout leaves card handling with Stripe. Secrets are separated by environment, and dependency changes are reviewed before an automated audit fix is accepted.
What I now check before shipping
The release path combines unit and security tests, browser suites, staging migrations, remote smoke tests and a manual pass. Chromium and WebKit cover repeatable flows. Camera switching, audio output, WebRTC and mobile Safari still require physical devices and sessions with actual users.
Operational checks grew out of the same incidents: D1 restore drills, R2 inventory checks, structured logs without personal data, recovery jobs for interrupted recording imports and runbooks for secret rotation or a failed deployment. Rolling back a Worker cannot undo a database migration or recall an email, so each of those effects needs its own recovery procedure.
Some decisions remain outside the codebase. Expanding commerce requires independent legal, privacy, fiscal and security review. Device coverage improves only as more lessons run on real hardware. Long-retention encrypted backups outside the Cloudflare account still need an owner and a key-management decision.
When I read the release checklist now, almost every line points back to a concrete failure: eight missing bytes, a silent audio route, a vanished cookie panel or a missing stylesheet. That history is a better description of the platform than its architecture diagram.
