Diego
← Back to projects

Byro

E-Book store — Laravel API + React, with Mercado Pago checkout.

Back-end (API)

  • PHP 8.3
  • Laravel 11
  • PostgreSQL 16
  • Sanctum
  • HMAC-SHA256
  • Mercado Pago SDK
  • Scramble (OpenAPI 3.1)
  • Pest 3

Front-end

  • React 18
  • Vite 6
  • React Router v7
  • Tailwind v3
  • Zustand
  • Axios
  • date-fns
  • Mercado Pago SDK React

Infra & Deploy

  • Docker
  • Docker Compose
  • Nginx
  • Caddy
  • Hetzner VPS
  • Vercel

Full-screen for best quality.

Context

Byro is a technical eBook store. The platform covers signup, token auth, catalog, cart, Mercado Pago checkout and PDF delivery via signed URL.

The back-end handles common scenarios in payment gateway integrations: out-of-order webhooks, coexistence of legacy IPN and Webhooks v2, HMAC signature validation affected by PHP’s native parser, and idempotent product release against multiple notifications for the same payment.

The front-end implements gateway integration (webhook, return and Mercado Pago’s three payment states).

Technical decisions

API · HMAC validation with defense in depth

Mercado Pago signs webhooks via HMAC-SHA256 over a manifest like id:{paymentId};request-id:{xRequestId};ts:{ts};. The idmust come from the raw query string — PHP’s parse_str() converts data.id to data_id automatically. Manual QUERY_STRING parsing preserves the parameter name.

MP varies the manifest format across topics and versions. Validation tests 6 variations against the received signature using hash_equals (timing-safe comparison). If none match, a config-driven loose_mode logs and proceeds by calling the MP API to confirm via access_token.

API · Signed URLs instead of direct authenticated download

Streaming the PDF directly from the authenticated route couples the download to the Sanctum token and breaks if the user changes tabs, opens on another device, or uses a download manager. The solution is a 30-minute signed URL via URL::signedRoute embedding ebookId and userEmail, validated by Laravel’s native signed middleware.

Result: zero state to manage, native expiration, and a personalized filename generated from the email embedded in the URL.

API · Scramble instead of l5-swagger

Documenting the API in OpenAPI with darkaonline/l5-swagger requires #[OA\Post(...)] and #[OA\Property(...)] attributes on every endpoint. That information already lives in routes/api.php, the FormRequest and the return type — repeating it all in attributes would duplicate the same definition in several places and make the docs easy to fall out of sync.

dedoc/scramble reads routes, FormRequests and return types automatically and generates OpenAPI 3.1 with zero annotations. Short PHPDoc per endpoint covers summary and @response for non-obvious status codes (404/409). Docs always in sync with code, same Swagger UI at /docs/api.

Front · Zustand + persist for the cart

With the migration from single product to dynamic catalog, the cart needed global state. Zustand solves it with a small store (~25 lines), granular selection per hook (useCartStore(s => s.items.length) only re-renders when count changes), and the persist middleware for localStorage sync.

The cart survives reload and logout — expected behavior for an unauthenticated visitor who filled the cart before signing up.