Part 1 of 3 in the Mes Recettes series: Part 2 – Architecture · Part 3 – Testing & Delivery | 🇫🇷 Version
Why This Project Exists
I wanted a fast, portable way to index my cookbook recipes—linking authors, books, pages, ratings, and notes—with minimal backend overhead. Instead of standing up an API + database manually, I paired Blazor WebAssembly (C# in the browser) with Supabase (managed PostgreSQL, Auth, Realtime, REST).
Goals:
- Single-page UX, instant navigation
- Strongly-typed models end to end
- Auth without reinventing flows
- A foundation to later add search, offline support, and real-time collaboration
Technology Choices (and Why)
Concern | Choice | Reason |
---|---|---|
UI & SPA runtime | Blazor WebAssembly (.NET 9) | Full-stack C#, component model, ecosystem maturity |
UI components | MudBlazor | Material look, dialogs, tables, theming |
Backend | Supabase | PostgreSQL + Auth + Realtime + REST + row security |
Data modeling | PostgREST + C# attributes | Direct mapping, fewer DTO layers initially |
Validation | DataAnnotations | Simple, built-in, testable |
Testing | xUnit | Standard .NET, parallel, attribute-driven |
Deployment (target) | Static hosting/CDN (e.g. Azure SWA / GitHub Pages + API) | Cheap + global distribution |
High-Level Architecture
The browser downloads the Blazor app once; subsequent data calls go straight to Supabase’s REST endpoints (via the official client SDK).
Core Code Excerpt
Program startup wires Supabase cleanly:
var options = new SupabaseOptions
{
AutoRefreshToken = true,
AutoConnectRealtime = true
};
builder.Services.AddSingleton(sp =>
new Client(
builder.Configuration["supabase:Url"] ?? string.Empty,
builder.Configuration["supabase:Key"] ?? string.Empty,
options
)
);
builder.Services.AddScoped<ISupabaseAuthWrapper, SupabaseAuthWrapper>();
builder.Services.AddScoped<AuthService>();
Auth wrapper keeps client concerns isolated:
public class SupabaseAuthWrapper : ISupabaseAuthWrapper
{
private readonly Supabase.Client _client;
public async Task<Session?> SignIn(string email, string password) =>
await _client.Auth.SignIn(email, password);
public async Task SignOut() => await _client.Auth.SignOut();
public User? CurrentUser => _client.Auth.CurrentUser;
}
Models map directly to tables via attributes:
[Table("recettes")]
public class Recipe : BaseModel
{
[PrimaryKey("id")] public int Id { get; set; }
[Column("name"), Required] public string Name { get; set; } = string.Empty;
[Column("rating"), Range(1,5)] public int Rating { get; set; }
[Column("notes")] public string? Notes { get; set; }
[Column("book_id")] public int? BookId { get; set; }
[Reference(typeof(Book), joinType: ReferenceAttribute.JoinType.Left, true)]
public Book? Book { get; set; }
}
What’s Implemented vs. Planned
Area | Status |
---|---|
Auth (email/password) | Implemented |
Models (Recipe / Book / Author / Junction) | Implemented |
Basic CRUD UI | Implemented (dialogs & pages) |
Rating validation (1–5) | Implemented + unit tests |
Repository / Unit of Work abstraction | Documented pattern (not yet wired) |
Caching layer | Planned (pattern drafted) |
Realtime updates | Enabled in config; targeted enhancement |
Full-text search & indexes | Planned (SQL examples drafted) |
CI/CD pipeline | Documented; workflow template pending |
Lessons So Far
- Supabase + Blazor cuts time-to-first-feature dramatically.
- Keeping models close to storage early is fine; abstractions can grow later.
- DataAnnotations remain “good enough” for first-phase domain constraints.
- MudBlazor accelerates UI but theming decisions should be centralized early.
Next Enhancements
- Introduce a
RecipeService
+ caching decorator - RLS policies enforcing per-user ownership (SQL authoring phase)
- Add integration tests hitting a staged Supabase instance
- Real-time listener to reflect concurrent edits
Source
GitHub: https://github.com/mongeon/RecettesIndex