A production member management portal for an Italian blood donor association — replacing a legacy CSV workflow with a full-stack SPA + REST API, containerized and deployed on a VPS. Live at portale.avisossolana.it, serving 4,000+ real members.
AVIS Ossolana ODV (Associazione Volontari Italiani Sangue, Domodossola) needed to replace a manual CSV-based workflow on a legacy server with a modern, accessible portal. The new system needed to work for non-technical staff managing 4,000+ blood donors, with mobile access and integrated communication tools — and it had to be trustworthy enough to hold real members' personal and health-adjacent data.
Screenshots from a local instance seeded with fabricated demo data — the interface is identical to production, but no real member is shown here.
{{variable}} syntax mapped to Meta's approved template format.
The system uses a decoupled frontend/backend architecture served through a single Nginx entry point. Everything runs containerized on a cloud VPS, with SSL handled at the Nginx layer.
Hover over each node to see the design rationale
Every choice was made considering the client context: non-technical admin staff, mobile access needs, Italian data sovereignty (GDPR), and budget constraints of a volunteer association.
The admin WhatsApp wizard has 5 sequential steps with complex state — React's component model handles this far better than server-rendered pages. TypeScript adds compile-time safety when dealing with member data structures.
Near-instant HMR during development and optimized production builds. Simpler configuration than webpack for a project of this size. Native ESM support aligns with the modern TypeScript setup.
Rapid prototyping with consistent design tokens. The custom AVIS red (#C8102E) is easily configured as a Tailwind color. Utility-first approach avoids naming CSS classes for dozens of small UI components.
Minimal APIs keep the backend lean — all route handlers in Program.cs, no controller boilerplate for an MVP with ~15 endpoints. Familiarity with the .NET ecosystem accelerated development. Chose over Node.js/Express for strong typing and future EF Core integration.
Chosen over SQL Server to avoid licensing costs for a non-profit client. Open-source, well-supported by EF Core, and excellent for GDPR-sensitive member data. The legacy CSV export was migrated in through EF Core migrations.
Single-domain deployment: Nginx serves the React build as static files and proxies /api/* to the .NET container. No CORS complexity. Docker Compose ensures identical dev/prod environments and easy server migration.
Italian hosting provider = data residency within Italy, simplifying GDPR compliance for member personal data. Cost-effective for a volunteer association budget. Italian-language support for the client.
Official WhatsApp Business API for bulk messaging. Chosen over Twilio for lower per-message cost at scale and direct Meta integration. The template system was built to match the Meta template variable format, and now includes webhook signature verification hardened during the security review.
Three role tiers with separate views and permissions — member, admin, and superadmin — plus a registration & approval workflow for new members and a GDPR-compliant privacy policy page.
{{variable}} syntax, plus a parallel email template systemAdmin staff needed to see exactly what each WhatsApp message would look like for each recipient before sending. With 100+ members and dynamic variables like {{nome}}, {{prossima_donazione}}, previewing each one individually would be unusable.
Template variables are substituted client-side in React using string replacement with the selected member's actual data. The preview step in the wizard renders the final resolved message for each recipient before the admin confirms. No backend round-trip needed for preview.
Running a React SPA and a .NET API on the same domain requires careful routing. React Router handles client-side navigation (all routes should return index.html). The API needs its own path prefix. Getting SSL, gzip, and SPA fallback all working together in one Nginx config was non-trivial.
Nginx serves the React build as static files with a try_files $uri /index.html fallback for SPA routing. Any request to /api/* is proxied to the .NET container. SSL is terminated at Nginx. Static assets have 1-year cache headers. All in a single nginx.conf.
The existing member database was a CSV export from a legacy Linux server with inconsistent formatting. Column names in Italian, dates in Italian format (DD/MM/YYYY), and missing fields on older records. A direct import would have produced dirty data for thousands of real members and their donation history.
A migration script handled the transformation: Italian date parsing, column mapping to the PostgreSQL schema, default value injection for missing fields, and duplicate tessera (card number) detection before the EF Core seed migration ran. All 4,000+ member records and their donation history were imported cleanly ahead of go-live.
Once the schema held real names, dates of birth, and donation/health-adjacent records for 4,000+ people, the MVP-era shortcuts couldn't ship: an XSS-exposed input, insecure fallback values for JWT/app secrets, database ports reachable outside the Docker network, and no signature verification on the incoming WhatsApp webhook.
A dedicated security review and hardening pass ran before production launch: sanitized/escaped the vulnerable input, removed insecure secret fallbacks in favor of required environment configuration, closed the exposed database ports at the Docker Compose network level, and added Meta webhook signature verification. Deployed only after the pass was clean.
The entire stack is containerized. Deploying to a new server is a single docker compose up -d --build command. SSL is handled outside the containers via certbot on the host.
Live in production at portale.avisossolana.it, replacing the legacy CSV workflow for the client's full membership. Marco owned the project end-to-end: architecture, development, security hardening, deployment, data migration, and ongoing production support.
Got a cloud project, an internal tool, or a web platform that needs building? Let's talk.