Introduction
If you've ever built a SaaS product, you know the nightmare of custom requirements. Club A wants to ask registrants for their T-shirt size. Club B needs to know their dietary restrictions. Club C requires a GitHub link. How do you design a database schema for a platform where the required data changes drastically for every single event?
When architecting EventX, a college event management platform, the standard approach would be to create a 'Participants' table with 50 nullable columns (e.g., `tshirt_size`, `github_url`, `food_pref`). This is a well-known anti-pattern known as 'Entity-Attribute-Value' (EAV) mapping, and it quickly leads to database bloat and impossible query logic.
The Relational Schema Dilemma
Relational databases (SQL) thrive on rigid, predictable structures. But event registration is inherently unpredictable. If we hardcode columns, the platform isn't scalable. If we allow users to create custom tables on the fly, we open ourselves up to catastrophic security and performance risks. We needed the flexibility of a NoSQL document store (like MongoDB) but the rigid transactional guarantees, relational joins, and Row Level Security (RLS) of PostgreSQL.
The theoretical solution wasn't to build a better table; it was to collapse the boundary between schema and state using PostgreSQL's binary JSON format.
Architecture
We implemented a Dynamic Form Builder that serializes custom UI states directly into PostgreSQL `JSONB` columns. When an event organizer drags and drops a 'Text Input' or 'Dropdown' onto their form, EventX generates a JSON schema defining the fields, validation rules, and types. This schema is saved into a single `form_schema` column in the `Events` table.
When a participant registers, their inputs are validated against that specific schema in memory, and the results are stored in a single `responses` JSONB column in the `Registrations` table. Because `JSONB` is stored in a decomposed binary format rather than plain text, PostgreSQL allows us to index and query inside the JSON tree natively. We can effortlessly run a query like 'Find all users where responses->>"tshirt_size" = "Large"' with index-backed millisecond latency. We achieved NoSQL flexibility with SQL ACID compliance.
What I prioritized
The power of mixing relational and document architectures:
- JSONB Serialization. Storing unpredictable state in binary JSON to prevent schema bloat.
- Schema-on-Read. Validating dynamic payload structures at the application layer rather than the database layer.
- GIN Indexing. Utilizing Generalized Inverted Indexes to achieve sub-millisecond query speeds inside JSON trees.
- ACID Compliance. Maintaining strict relational joins for auth and payments while enabling NoSQL-like flexibility.


