Introduction
Building a personal tool that solves a real problem is one of the most rewarding experiences in software development. When you combine that with the challenge of making it accessible anywhere, the project transforms from a simple utility into a full-fledged application with cloud architecture, authentication, and performance considerations.
Spaced repetition systems (SRS) are among the most effective learning tools available, backed by decades of cognitive science research. These systems schedule reviews at optimal intervals to maximize retention while minimizing study time. For developers working with .NET and Azure, building a custom flashcard app offers an excellent opportunity to implement an SRS while exploring modern cloud patterns.
This article walks through the complete development and deployment journey of a spaced repetition flashcard application. Starting from local development with Blazor WebAssembly, through implementing the SM-2 algorithm, to deploying on Azure with a focus on cost optimization and security best practices.
The Architecture: Blazor WebAssembly Meets Azure
The tech stack chosen for this application reflects a pragmatic approach to modern .NET development. Blazor WebAssembly runs C# code directly in the browser, enabling full-stack development without context-switching between languages. For .NET developers, this represents a significant productivity advantage, as the same models and validation logic can be shared between frontend and backend.
The complete stack comprises Blazor WebAssembly (.NET 10) for the frontend, ASP.NET Core minimal APIs for the backend, and Azure services for hosting and data persistence. This architecture promotes clear separation of concerns: the frontend handles user interaction and state management, while the backend manages data operations and business logic.
src/
├── Frontend/ # Blazor WebAssembly application
│ ├── Components/ # Reusable UI components
│ ├── Pages/ # Routeable pages with [Authorize] attributes
│ └── Services/ # HTTP clients and state management
├── Backend/ # ASP.NET Core Minimal API
│ ├── Endpoints/ # API route definitions
│ ├── Models/ # Shared data models
│ └── Services/ # Business logic (SM-2, import handling)
└── Shared/ # Common models and validation
The frontend is hosted on Azure Static Web Apps, which provides built-in CI/CD through GitHub Actions and automatic SSL certificates. The backend runs on Azure App Service, while Azure SQL Database handles data persistence. This separation allows independent scaling and updates, though for a personal application, the free tiers are more than sufficient.
Excel Import: Making Data Migration Seamless
One of the key features that makes this application practical is the ability to import flashcards directly from Excel spreadsheets. Many learners already maintain vocabulary lists or study materials in Excel, making a frictionless import path essential.
The import functionality uses ClosedXML, a .NET library for reading and writing Excel files without requiring Excel to be installed. The endpoint accepts a multipart form upload and processes the file in memory:
app.MapPost("/collections/{id}/import", async (int id, HttpRequest request, FlashcardsDbContext db) =>
{
var file = request.Form.Files[0];
using var workbook = new XLWorkbook(file.OpenReadStream());
var sheet = workbook.Worksheet(1);
var headerRow = sheet.Row(1);
int? frontCol = null, backCol = null, notesCol = null;
foreach (var cell in headerRow.CellsUsed())
{
switch (cell.GetString().Trim().ToLowerInvariant())
{
case "front": frontCol = cell.Address.ColumnNumber; break;
case "back": backCol = cell.Address.ColumnNumber; break;
case "notes": notesCol = cell.Address.ColumnNumber; break;
}
}
// Build and save cards from subsequent rows
});
Column detection is case-insensitive, reducing friction for users. The application also provides a template download function, allowing users to understand the expected format before creating their own import files. This attention to user experience transforms what could be a frustrating data entry process into a simple two-step operation.
The SM-2 Algorithm: The Science Behind Spaced Repetition
The SM-2 algorithm, developed by Piotr Wozniak in the 1980s, remains the foundation for most spaced repetition applications, including Anki. Despite its age, the algorithm’s simplicity and effectiveness have made it remarkably resilient, with the original constants still used in modern implementations.
At its core, SM-2 maintains three pieces of data per flashcard:
Easiness Factor (EF): A floating-point value representing how naturally the card’s content comes to the user. Initially set to 2.5, it adjusts based on recall quality.
Repetition Count: The number of times the card has been successfully recalled.
Interval: The number of days until the next review.
// quality: 0=Again, 1=Easy, 2=Normal, 3=Hard
float newEf = currentEf + (0.1f - (5 - sm2Quality) * (0.08f + (5 - sm2Quality) * 0.02f));
newEf = Math.Max(1.3f, newEf); // EF never drops below 1.3
newInterval = currentRepetitions switch
{
0 => 1, // first review: come back tomorrow
1 => 6, // second review: come back in 6 days
_ => (int)Math.Round(currentInterval * currentEf) // growing intervals after that
};
When a user rates their recall, the quality value determines how the algorithm adjusts. “Again” ratings (quality 0) reset the repetition count to zero, sending the card back to day one. “Easy” ratings reinforce the interval, eventually pushing less challenging cards months or years into the future.
Focus Enhancement: The 45-Second Timer
One behavioral addition that significantly improves the learning experience is a 45-second timer per card. This constraint prevents the common pitfall of losing focus or opening other tabs while a card is displayed. If the timer expires before the user completes the card, it automatically counts as “Again,” resetting the repetition progress.
This timer serves two purposes:
Maintaining Engagement: The time pressure encourages focused, deliberate recall rather than passive recognition.
Preventing Procrastination: It prevents users from artificially inflating their performance by taking excessive time to recall.
From a UX perspective, the timer is displayed prominently during reviews, with a subtle visual indicator that becomes more prominent as time runs out. The CSS is specifically designed to be mobile-friendly, ensuring a consistent experience across devices.
Azure Deployment Strategy: Cost Optimization for Personal Projects
For personal applications where cost is a primary concern, selecting the right Azure services and tiers can make a significant difference. This deployment strategy uses free tiers where possible, with only the database incurring a minimal monthly cost.
Azure Static Web Apps: The frontend is hosted on the Free tier, which includes automatic GitHub Actions-based deployments on every push to the main branch. Static Web Apps provides automatic SSL, custom domain support, and global CDN distribution at no cost.
Azure App Service F1: The backend minimal API runs on the Free tier, which offers 60 minutes of CPU compute per day. For a lightweight flashcard application that sees at most an hour of daily usage, this is more than adequate.
Azure SQL Basic DTU: The database is the only paid component at approximately $5/month. This tier provides 5 DTUs and 2GB of storage, suitable for small applications with infrequent usage patterns.
// Bicep infrastructure definition - single file deploys all resources
param location string = resourceGroup().location
param entraAdminLogin string
param entraAdminObjectId string
resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = {
name: 'flashcards-sql-${uniqueString(resourceGroup().id)}'
location: location
properties: {
administratorLogin: entraAdminLogin
administratorLoginPassword: '...' // replaced with secure parameter
administrators: {
login: entraAdminLogin
sid: entraAdminObjectId
azureADOnlyAuthentication: true
}
}
}
resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-11-01' = {
parent: sqlServer
name: 'FlashcardsDb'
sku: { name: 'Basic', tier: 'Basic' }
properties: { maxSizeBytes: 2147483648 } // 2GB
}
The developer started with Azure SQL Serverless tier expecting the auto-pause feature to save costs. However, the minimum billing window meant the service stayed around $17/month. The Basic DTU tier proved simpler and cheaper for light daily use.
Infrastructure as Code with Bicep
Managing Azure resources manually through the portal is acceptable for one-off deployments, but for reproducibility and version control, infrastructure as code (IaC) is superior. Bicep, Azure’s domain-specific language for resource deployment, allows defining all resources in a single file:
Deploy entire stack with two commands
az group create --name Flashcards --location japaneast
az deployment group create --resource-group Flashcards --template-file infra/main.bicep --parameters entraAdminLogin="you@example.com" entraAdminObjectId=""
The Bicep file provisions: the SQL server and database, App Service plan and API instance (with Managed Identity pre-configured), and Static Web App instance. The only manual post-deployment step is granting the Managed Identity access to the SQL database—a few SQL commands that are documented in the repository.
Passwordless Authentication with Managed Identity
Traditional connection strings with usernames and passwords create security risks: secrets can be accidentally committed to version control or leaked through logs. Azure Managed Identities provide a passwordless alternative where the application authenticates directly through its Azure identity.
// Connection string uses Active Directory Managed Identity authentication
"Server=tcp:your-server.database.windows.net,1433;Initial Catalog=your-database;Authentication=Active Directory Managed Identity;Encrypt=True"
The SQL server is configured to trust the App Service’s Managed Identity:
CREATE USER [your-app-service] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [your-app-service];
ALTER ROLE db_datawriter ADD MEMBER [your-app-service];
For local development, the application uses SQL Server LocalDB with a trusted connection, loaded from a gitignored .env file. This approach eliminates password management while maintaining secure access patterns.
User Authentication with Microsoft Entra ID
Securing the API endpoints is critical once the application is publicly accessible. The application uses Microsoft Entra ID (formerly Azure AD) with the PKCE (Proof Key for Code Exchange) flow, the standard OAuth approach for browser-based applications that cannot safely store a client secret.
The frontend uses Microsoft.Authentication.WebAssembly.Msal to redirect users to Microsoft’s login page and receive an ID token on return. The backend validates the JWT on every request using Microsoft.Identity.Web. The client ID and tenant ID are injected into appsettings.json during the GitHub Actions build process.
A subtle issue encountered: in Blazor WASM, AuthorizeRouteView invokes its handler for any page with [Authorize] when the user isn’t authenticated. The login callback page is inherently unauthenticated when it loads because the token hasn’t arrived yet. If the callback page triggers , it stores the callback URL as the post-login redirect target, causing an infinite login loop.
The fix is to declare [Authorize] explicitly on each page that needs protection rather than relying on a blanket global handler. The handler then only fires for those specific pages, while the callback page completes the login flow normally.
Best Practices
Start with Infrastructure as Code: Define all Azure resources in Bicep or ARM templates to ensure reproducible deployments. The ability to spin up a complete environment with two commands is invaluable for personal projects where you might rebuild or migrate.
Use Passwordless Authentication: Managed Identities eliminate secret management and reduce security risks. Set up Managed Identity for your App Service and grant it SQL access rather than storing connection strings with credentials.
Consider Cost Tiers Carefully: The Azure SQL Basic DTU tier at $5/month is often more cost-effective than Serverless for light usage patterns. Serverless might auto-pause, but the minimum billing window can result in higher costs.
Implement Timer Focus Constraints: For any application requiring sustained attention, time constraints can prevent procrastination and improve outcomes. A 45-second timer was effective for flashcard reviews, but similar principles apply to quizzes, reading exercises, or focus sessions.
Validate Column Detection Case-Insensitively: When implementing import functionality, make column detection flexible to user variations. Case-insensitive matching and template downloads reduce user frustration.
Common Mistakes
Overlooking the Authentication Callback Flow: In Blazor WASM applications, improperly configured AuthorizeRouteView can create login loops. Always set [Authorize] on specific pages rather than relying on blanket authorization handlers.
Choosing the Wrong SQL Pricing Model: Many developers gravitate toward Serverless thinking it will save money, but the minimum billing window means it can cost more than Basic DTU for low-usage applications. Evaluate actual usage patterns before selecting a tier.
Hard-Coding Connection Strings: Storing secrets in configuration files is a security anti-pattern. Use Managed Identity for production and environment variables or user secrets for local development.
Manual Resource Management: Creating resources through the portal without documenting the process makes redeployment difficult. Use Bicep or ARM templates to codify infrastructure decisions.
Neglecting Mobile-Friendly Design: If the application is intended for mobile use, ensure CSS is responsive. Design for mobile-first and test on actual devices.
Final Thoughts
Building and deploying a spaced repetition flashcard application on Azure demonstrates the power of modern .NET development combined with cloud services. The ability to create a full-stack application using C# throughout the stack, with minimal infrastructure management, makes personal projects accessible to developers without extensive cloud expertise.
The cost profile—free frontend and backend hosting with a $5/month database—makes this approach viable for learning projects, personal utilities, and side projects. The infrastructure as code approach using Bicep ensures that the entire environment can be reproduced with minimal effort.
For developers looking to expand this concept, consider adding features like:
Multi-tenancy for sharing decks with other users
Advanced analytics on recall patterns
Integration with other learning tools via APIs
Export functionality for backup and portability
The complete source code and deployment scripts provide a template for building and deploying similar applications, adapting the patterns to different domains, and exploring the capabilities of Azure’s free tiers. Whether for learning a language, studying for certifications, or building a custom knowledge management system, the principles covered here provide a solid foundation.
Top comments (0)