Calendário Mineral
A public calendar of regulatory deadlines and industry events for Brazil’s mining sector. You can browse by month, open an event, save it to Google Calendar, or sign up for a monthly email digest.
I wrote 240 of the repository’s 241 commits, across the back end, the email pipeline, and the front end.

The problem
Mining companies in Brazil work against recurring regulatory deadlines: ANM filings such as RAL, DIPEM, and DIEF-CFEM, environmental reports for IBAMA such as the RAPP, plus the sector’s conferences and trade fairs. Each deadline comes from a different agency. ÍGNEA wanted one public calendar for these dates, where someone on the team could publish events and subscribers would get a monthly summary by email.
Decisions
One digest per month, enforced by the database
The digest table has a unique month column, and the job checks for that month’s row before sending anything. Retries lock the row with SELECT FOR UPDATE and refuse digests already marked as sent, so a second run in the same month or two retry clicks at once cannot email subscribers twice.
Validate before sending, and alert on failure
The email HTML and every image URL are checked before the first batch goes out. A failure stores the draft with its errors and emails the admin. In February 2026 this check blocked the month’s digest over validator rules that do not apply to email markup, and the failure could not be saved because the status column did not allow it. I turned those rules off with a comment on each, fixed the status constraint in a migration, and kept the check.
Send before the month starts
The digest first went out on the first Monday of each month with that month’s events. In April 2026 I moved it to the last Monday with the next month’s events, so deadlines in the first days of a month reach readers before that month begins.
What I built
- The public calendar in React: I connected it to the API, added React Router deep links for each event, a mobile month view with react-swipeable, lazy-loaded routes, and moved Tailwind CSS from a CDN script to a Vite build.
- The Express and PostgreSQL back end: event and category CRUD, JWT login with bcrypt-hashed passwords and role checks, image upload with Multer, and SQL migrations for each schema change (issuing agency field, blog link, digest status).
- The digest pipeline: node-cron scheduling, an HTML email template, html-validate checks tuned for email markup, sending through the Brevo API in batches of 99 with recipients in BCC, and exponential backoff with up to three attempts per batch on rate limits, server errors, and network failures.
- An admin area behind a hidden /admin route, with event editing, a digest preview for any month, test sends to one address, send history, and a retry button for failed or partial sends.
- Shareable event links: Express middleware that detects crawlers with isbot and injects Open Graph and Twitter Card tags, with 1200x630 preview images generated by Sharp.
- An image step in Sharp that resizes each upload to at most 1920 px wide without upscaling, converts it to WebP, and deletes the original file.
How it works
The front end is a React 19 single-page app built with Vite. Visitors browse events by month, filter by group, search, and open an event in a modal or on its own page at /events/:id. On phones the month view changes with a swipe. The interface is in Portuguese and English, and each event stores its title and description in both languages.
An Express API in TypeScript serves the public event data and a protected admin area. Events, categories, admin users, and the digest records live in PostgreSQL, and each schema change is a numbered SQL migration. When someone shares an event link, Express checks the user agent: social media crawlers get HTML with Open Graph tags and a preview image, and everyone else gets the regular app. The sign-up form adds the address as a contact in Brevo, which holds the subscriber list.
A node-cron job runs every Monday at 11:30, São Paulo time. On the last Monday of the month it collects the next month’s published events, skips the send if there are none, renders the digest email, validates the HTML and the image URLs, reads the subscriber list from Brevo, and sends the email through the Brevo API. Each run is recorded in a digest table with its status (sent, partial, or failed), and the admin area uses that table to preview, test, retry, and review past sends.



Built with
- Front end
- React 19, TypeScript, Vite, React Router 7, Tailwind CSS 4, react-swipeable, react-hot-toast
- Back end
- Node.js, Express, TypeScript (tsx), JWT, bcrypt, Multer, Sharp, isbot
- Data
- PostgreSQL (pg), SQL migrations
- Email and scheduling
- Brevo API, node-cron, html-validate, exponential-backoff
- Infrastructure
- AWS Lightsail (Ubuntu), PM2