Implementing Progressive Feature Unlocks in SUClassroom
Designing complex software often feels like building a tree: you start with a solid foundation, and as users engage, you branch out into new, specialized functionalities. In the context of the SUClassroom project, we recently reached a milestone where we needed to introduce a structured way to gate content—specifically, phase-based progress unlocks.
The Challenge of Linear Progression
When building learning or classroom management tools, progression isn't just about showing the next button. It is about state management. You need to ensure that a student has completed the requirements for 'Phase N' before they can access the materials for 'Phase N+1'.
We wanted a system that was performant, easy to audit, and integrated seamlessly with our existing Drizzle ORM and Supabase stack. Relying on simple client-side toggles would be a security oversight; the gatekeeping must happen at the database level.
Implementing Secure Unlocks
To handle this, we opted for a schema-backed approach. We define the user's progress in our database, ensuring that any request for content is validated against their actual completion status.
-- Schema for tracking user phase completion
CREATE TABLE user_phase_progress (
user_id UUID REFERENCES users(id),
phase_id INT,
completed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (user_id, phase_id)
);
By leveraging this table, we can write clean, reactive logic in our TypeScript services to verify access before rendering the next component.
// Example service to check if a user can access a phase
export async function canAccessPhase(userId: string, phaseId: number) {
const progress = await db
.select()
.from(userPhaseProgress)
.where(eq(userPhaseProgress.userId, userId));
// Check if all previous phases are completed
return progress.length >= phaseId - 1;
}
Why This Matters
Using this pattern, we avoid 'prop-drilling' complex state through our React component tree. Instead, the server acts as the single source of truth. If a user tries to jump ahead, the API layer—protected by standard JWT validation—simply denies the request, and the UI remains locked.
This approach gives us the flexibility to add new phases in the future without refactoring our core logic. By decoupling the 'completion state' from the 'UI rendering', we ensure that SUClassroom remains scalable and easy to maintain as we add more phases to the curriculum.
The Takeaway
When building progression systems, don't rely on the UI to hide what the backend doesn't protect. Use a relational database to keep your progress logic simple, secure, and predictable.
Generated with Gitvlg.com