Post

Etamyd

Etamyd is live at etamyd.com.

Etamyd (Arabic: منصة اعتمد) is a multi-tenant platform for training organizations. Each organization manages its programs, course runs, enrollment and waitlists, attendance, trainers, and online registration forms in isolated data. It issues bilingual certificates from JSON-configured templates, and each certificate carries a QR code that anyone can scan to verify it. The product is Arabic-first, with right-to-left layout, and also in English.

It was built by a team of three: two engineers and a product manager. The product manager gathers and owns the requirements. I am one of the two engineers. I own a set of features (listed below), and the two engineers review each other’s pull requests on every change.

My work

  • Trainer invite and trainer portal — an organization admin invites a trainer by email. The trainer sets a password from the link and gets a read-only portal with their profile and assigned course and program runs. Invite tokens are stored only as a hash, expire, and work once; a new invite cancels the older ones in the same transaction. Account creation, trainer link, and invite use happen in one Prisma transaction, with rate limits on resend and accept.
  • Online registration forms — a form builder, a public share link for each form, and a response review. Public submissions have rate limits and de-duplication, and an email-conflict check runs inside the approve transaction.
  • Security and correctness fixes, each with regression tests:
    • A rate-limited request looked like a success, so the public form showed its thank-you screen for a submission that was never stored. Each rate-limit middleware now has a client part that rejects with a typed error and honours Retry-After.
    • The rate limiter read the client IP from the request body. The server now derives the IP itself.
    • Two bulk paths could skip the waitlist seat limit. One guard now covers every path.
  • Bulk participant import and export (Excel) — a template with a protected instruction sheet, server-side validation, duplicate detection inside the file, and an all-or-nothing import with PostgreSQL savepoints.
  • Organization administration — member management behind admin-only middleware, organization settings, a theme colour per organization, account settings, and soft delete across all entities (every query filters to active records, and soft-deleted users cannot sign in).
  • Public certificate verification — I built the first version of the QR target page, with automatic verification from the URL and a per-IP rate limit.
  • Test and CI base — the first unit and integration tests, unit tests across the server functions, the Docker-based Playwright stack and its seed script, and the GitHub Actions workflows for typecheck, tests, security audit, and Dependabot.
  • Landing page and brand rollout — the public landing page (pricing, program showcase, in-page navigation, Arabic RTL and English) and the logo set across the app, emails, favicon, PWA icons, and social card.

Platform (team-built)

  • Multi-tenancy — organization-isolated data.
  • Access control — a server-function middleware chain over role enums (4 organization roles and 2 platform roles). Better Auth supplies authentication and sessions.
  • Certificate templates — JSON-configured text, image, and QR fields; A4 or Letter; Arabic, English, or bilingual.
  • Bilingual — Arabic RTL and English through Paraglide JS, with 1,655 message keys per language.
  • Stack — TanStack Start with Router, Query, Table, and Form, the React Compiler, PostgreSQL + Prisma, Zod, Tailwind CSS + Shadcn UI; tested with Vitest and Playwright; deployed with Docker.

Summary

A live, multi-tenant, bilingual platform for training organizations, with QR-verified certificates.

  • Built in a two-engineer team with a product manager, with PR review on every change.
  • My features: trainer invite and portal, registration forms, bulk import, organization administration, the first verification page, the test and CI base, and the landing page.
  • Security fixes with regression tests: rate-limit handling, server-derived client IP, and a single waitlist guard.
This post is licensed under CC BY 4.0 by the author.