Skip to content

Architecture Decision Records

This document contains Architecture Decision Records (ADRs) for Tabula Lens, capturing key architectural decisions, their context, and consequences.

Architecture Decision Records (ADRs) document important architectural decisions made during the development of a system. Each ADR captures:

  • Context: The situation that led to the decision
  • Decision: The decision that was made
  • Consequences: The results of the decision, both positive and negative

Accepted

Tabula Lens needed to work with any frontend technology and multiple backend frameworks (Express, Fastify, Next.js, etc.). A direct database connection from the frontend would expose credentials and create security vulnerabilities.

Implement an HTTP API as the universal interface between frontend and backend. The backend handles database connections and exposes a RESTful API that any frontend can consume.

Positive:

  • Frontend-agnostic: Any frontend technology can be used
  • Security: Database credentials never leave the backend
  • Flexibility: Backend can be implemented in any language/framework
  • Scalability: API can be cached, load-balanced, and monitored
  • Standard: Uses well-understood HTTP/REST patterns

Negative:

  • Additional layer: Adds HTTP overhead compared to direct connection
  • Complexity: Requires API implementation and maintenance
  • Latency: Network latency between frontend and backend
  • State management: HTTP is stateless, requires session/token management
  1. Direct Database Connection: Rejected due to security concerns
  2. GraphQL: Considered but REST chosen for simplicity and broader compatibility
  3. WebSocket: Rejected as overkill for query/response pattern

Superseded by ADR-007: Multi-Database Support Decision

Tabula Lens needed a database that could handle structured data, support complex queries, and provide strong consistency guarantees. The database needed to be widely supported, well-documented, and suitable for production use.

Use PostgreSQL as the primary and supported database for Tabula Lens.

This decision was later superseded by ADR-007: Multi-Database Support Decision, which extended Tabula Lens to support MySQL, SQLite, and SQL Server in addition to PostgreSQL. PostgreSQL remains the recommended database for new projects due to its advanced features, but Tabula Lens now provides first-class support for multiple database engines through a unified query layer.

Positive:

  • Powerful: Advanced features (JSON, indexes, constraints, etc.)
  • Reliable: ACID compliance and strong consistency
  • Performant: Excellent query optimization and indexing
  • Widely Supported: Available on all major cloud platforms
  • Open Source: No licensing costs
  • Extensible: Supports custom functions and extensions

Negative:

  • Single Database: Limits database flexibility (though other databases could be added)
  • Resource Intensive: Requires more resources than lighter databases
  • Complexity: Advanced features have learning curve
  1. MySQL: Good alternative but PostgreSQL chosen for advanced features
  2. SQLite: Rejected for production use cases
  3. MongoDB: Rejected as Tabula Lens focuses on structured data
  4. Multi-Database Support: Deferred to future versions

Accepted

The DatabaseViewer React component was becoming monolithic, making it difficult to maintain, test, and customize. Users needed flexibility to customize individual UI elements while maintaining the overall component functionality.

Refactor the DatabaseViewer component into a modular architecture with:

  • Sub-components (LoadingState, ErrorState, EmptyState, etc.)
  • Custom hooks (useLogger, useTableState, useDatabaseData)
  • Utility functions (fetchHelpers, validationHelpers, styleHelpers)
  • Runtime prop validation

Positive:

  • Maintainability: Easier to understand and modify
  • Testability: Individual components can be tested in isolation
  • Customizability: Users can override specific sub-components
  • Reusability: Hooks and utilities can be used independently
  • Performance: React.memo optimization on sub-components
  • Developer Experience: Better code organization and documentation

Negative:

  • Complexity: More files and components to manage
  • Learning Curve: Users need to understand the architecture
  • Bundle Size: Slightly larger due to modular structure
  • Migration: Existing users need to understand new structure
  1. Keep Monolithic Component: Rejected due to maintainability concerns
  2. Separate Package: Rejected as overkill for this use case
  3. Higher-Order Components: Rejected in favor of hooks pattern

ADR-004: CSS Custom Properties for Theming

Section titled “ADR-004: CSS Custom Properties for Theming”

Accepted

Tabula Lens needed a theming system that would:

  • Support dark mode
  • Allow brand customization
  • Work across different frontend frameworks
  • Enable runtime theming without JavaScript
  • Maintain consistency across components

Use CSS custom properties (CSS variables) as the foundation of the theming system, with a --tlens- prefix to avoid conflicts.

Positive:

  • Runtime Theming: Theme changes without JavaScript re-render
  • Dark Mode: Native support through CSS media queries
  • Framework Agnostic: Works with any CSS-based framework
  • Performance: No JavaScript overhead for theme switching
  • Maintainability: Single source of truth for design tokens
  • Customization: Easy for users to override specific tokens

Negative:

  • Browser Support: Requires modern browsers (IE11 not supported)
  • Fallbacks: Need fallback values for older browsers
  • Complexity: CSS custom properties have learning curve
  • Debugging: Can be harder to debug than preprocessor variables
  1. CSS Preprocessors (Sass/Less): Rejected as they require build step
  2. JavaScript-based Theming: Rejected due to performance overhead
  3. CSS-in-JS: Rejected for framework agnosticism requirements

Accepted

Tabula Lens needed a logging system that would:

  • Provide visibility into database operations
  • Help with debugging and troubleshooting
  • Support different environments (development, production)
  • Allow sensitive data masking
  • Support multiple log formats (JSON, text, pretty)

Implement a comprehensive logging system with:

  • Multiple log levels (error, warn, info, debug, silent)
  • Configurable log formats
  • Request and query logging
  • Sensitive data masking
  • Environment-specific defaults
  • Custom logger integration

Positive:

  • Debugging: Easier to troubleshoot issues
  • Monitoring: Better visibility into system behavior
  • Security: Sensitive data can be masked
  • Flexibility: Users can integrate their own loggers
  • Production-Ready: JSON format for log aggregation
  • Configurable: Can be adjusted per environment

Negative:

  • Complexity: Additional configuration required
  • Performance: Logging has overhead (though minimal)
  • Storage: Logs need storage and rotation strategy
  • Noise: Can generate large amounts of log data
  1. Console.log Only: Rejected as insufficient for production
  2. Third-Party Logger: Considered but custom implementation chosen for control
  3. No Logging: Rejected as it would make debugging impossible

Accepted

Tabula Lens needed to support multiple Node.js frameworks (Express, Fastify, Koa, Next.js, etc.) while maintaining a consistent API. Each framework has different middleware patterns and request/response handling.

Implement a framework adapter pattern that provides:

  • Consistent API across frameworks
  • Framework-specific adapters
  • Middleware integration
  • Request/response handling
  • Authentication hooks

Positive:

  • Flexibility: Supports 15+ frameworks out of the box
  • Consistency: Same API regardless of framework
  • Maintainability: Framework-specific code isolated
  • Extensibility: Easy to add new framework adapters
  • Documentation: Clear examples for each framework

Negative:

  • Maintenance: Need to maintain multiple adapters
  • Testing: Each adapter needs testing
  • Complexity: Additional abstraction layer
  • Learning: Users need to understand adapter pattern
  1. Framework-Specific Packages: Rejected due to maintenance overhead
  2. Core Package Only: Rejected as it would limit adoption
  3. Community Adapters: Rejected as it would create inconsistency

Accepted

Tabula Lens started with PostgreSQL as the primary supported database. As adoption grew, users requested support for MySQL, SQLite, and Microsoft SQL Server because those engines were already in use in their existing environments. Supporting multiple relational databases would broaden adoption without changing the frontend or the public TabulaLens API.

Support PostgreSQL, MySQL, SQLite, and Microsoft SQL Server through a unified query layer:

  • Use Knex.js as the shared query builder.
  • Auto-detect the database type from the connection URL via detectDatabaseType.
  • Allow explicit type override in the TabulaLensConfig object.
  • Map each DatabaseType to the correct Knex client and underlying driver:
    • pg -> pg
    • mysql -> mysql2
    • sqlite -> better-sqlite3
    • mssql -> tedious
  • Isolate engine-specific SQL and metadata behavior behind the DialectStrategy interface.

Positive:

  • Broader Adoption: Works with the four most common relational databases
  • Unified API: Frontend and backend consumers use the same interface regardless of engine
  • Extensibility: New relational engines can be added by implementing a dialect
  • Ecosystem: Knex provides mature, well-tested multi-database support

Negative:

  • Maintenance Overhead: Four drivers and dialects to test and maintain
  • Driver Quirks: Each engine has subtle differences in metadata, types, and operators
  • Test Matrix: Unit and integration tests must cover all supported engines
  • Dependency Management: Users must install the correct peer driver for their database
  1. PostgreSQL Only: Rejected because it would limit adoption in MySQL/SQL Server/SQLite environments
  2. Separate Packages Per Database: Rejected due to fragmentation and duplicated code
  3. Community Adapters: Rejected because inconsistent adapter quality would harm the unified API
  4. Raw Driver Abstraction Without Knex: Rejected due to the amount of duplicated SQL and connection logic required

Accepted

To support multiple databases, Tabula Lens needed a query builder or ORM that could run against PostgreSQL, MySQL, SQLite, and SQL Server. Modern TypeScript ORMs such as Drizzle and Prisma are attractive because they offer type safety, but they typically require a predefined schema and generated client code. Tabula Lens introspects arbitrary tables at runtime and must work with any existing schema without code generation.

Use Knex.js as the query builder.

  • Knex is schema-agnostic: it does not require a generated schema or client.
  • It supports all four target databases through the same API.
  • It allows raw SQL fallbacks when engine-specific syntax is required.
  • It handles connection pooling and driver loading transparently.

Positive:

  • Runtime Introspection: Works with any existing database schema without code generation
  • Broad Database Support: PostgreSQL, MySQL, SQLite, and SQL Server are first-class clients
  • Flexibility: Raw queries are available when dialect differences cannot be abstracted
  • Maturity: Knex has a large ecosystem and stable API

Negative:

  • No Schema-Derived Type Safety: Query results are not statically typed from a schema
  • Manual Query Construction: Complex queries still require careful construction
  • Dialect Differences: Some behaviors (e.g., ILIKE vs LIKE, information_schema vs PRAGMA) must be handled explicitly
  1. Drizzle ORM: Rejected because it requires a schema-first approach and generated code
  2. Prisma: Rejected because it requires a schema file and client generation, which conflicts with runtime introspection
  3. TypeORM: Considered but rejected due to configuration complexity and inconsistent cross-database behavior
  4. Raw Database Drivers Only: Rejected because it would duplicate connection pooling, query building, and dialect handling

Accepted

Each supported database has different metadata catalogs, case-sensitivity rules, and text-search operators. PostgreSQL uses ILIKE and information_schema in the public schema. MySQL also uses information_schema but scopes it to DATABASE(). SQLite has no information_schema and instead uses PRAGMA table_info(...) and sqlite_master. SQL Server uses information_schema but has its own type names and collations. Embedding these differences directly into TabulaLens would create brittle, hard-to-test conditional logic.

Encapsulate all engine-specific behavior behind a DialectStrategy interface and a createDialect factory.

The interface defines four responsibilities:

  • getTables(db) — list all user tables
  • getColumns(db, table) — list column names and types for a table
  • getFilterableTypes() — return the type names considered text-searchable
  • getLikeOperator() — return LIKE or ILIKE for case-insensitive filtering

Implementations are provided for PostgreSQL, MySQL, SQLite, and SQL Server. TabulaLens instantiates the correct dialect once at startup and delegates metadata and operator decisions to it.

Positive:

  • Isolation: Engine-specific SQL is centralized in one place per database
  • Testability: Each dialect can be unit tested independently
  • Extensibility: Adding a new database only requires a new dialect implementation
  • Simplicity: TabulaLens query logic stays generic and readable

Negative:

  • Additional Abstraction: Developers must understand the strategy pattern to add a new engine
  • Duplicated Metadata Concepts: Similar information_schema queries exist in multiple dialects with small variations
  • Naming Collisions: Type names differ between engines and must be normalized carefully
  1. Inline Conditionals in TabulaLens: Rejected because it would scatter database-specific logic throughout the query builder
  2. Single Dialect with Raw Overrides: Rejected because it would still require engine checks and would not scale
  3. ORM Metadata API: Rejected because ORMs abstract metadata in ways that do not always map to the raw catalogs we need
  4. Knex-Specific Plugins: Considered but rejected because a custom interface gives us precise control over behavior

ADR-010: Peer Dependency Approach for Database Drivers

Section titled “ADR-010: Peer Dependency Approach for Database Drivers”

Accepted

Tabula Lens supports four database engines, each requiring a native or database-specific driver (pg, mysql2, better-sqlite3, tedious). Bundling all four drivers as required dependencies would force every user to install packages they do not need, including native build toolchains for SQLite (better-sqlite3) even if they only use PostgreSQL.

Declare database drivers as optional peer dependencies.

  • pg: ^8.0.0
  • mysql2: ^3.0.0
  • better-sqlite3: ^12.0.0
  • tedious: ^20.0.0

Knex loads whichever driver corresponds to the configured client at runtime. Users install only the driver(s) for the database(s) they connect to. Missing drivers produce a runtime error from the underlying Knex client when a connection is first attempted.

Positive:

  • Smaller Installs: Users are not forced to download drivers for engines they do not use
  • No Unnecessary Native Builds: SQLite native compilation is only required for SQLite users
  • Flexibility: The same package supports local SQLite development, managed PostgreSQL/MySQL production, and SQL Server deployments
  • Clear Ownership: Users explicitly choose and install the driver they need

Negative:

  • Manual Installation Step: Users must install a driver in addition to @tabula-lens/node
  • Runtime Errors: A missing driver is only detected when a query runs, not at install time
  • Peer Dependency Warnings: Package managers may warn about unmet peer dependencies for unused engines
  • Documentation Overhead: Installation instructions must list each driver separately
  1. Bundle All Drivers as Required Dependencies: Rejected because it would bloat installs and require native build tools for every user
  2. One Package Per Database Engine: Rejected because it would fragment the codebase and complicate the public API
  3. Optional Dependencies: Considered but rejected because optional dependencies still attempt installation and may fail on unsupported platforms
  4. Runtime Dynamic Imports: Rejected because it would complicate bundling and type checking without solving the peer-dependency problem

ADR-011: Manual API Documentation Over TypeDoc

Section titled “ADR-011: Manual API Documentation Over TypeDoc”

Accepted

Tabula Lens needed comprehensive API documentation for both Node and React packages. The codebase had extensive JSDoc comments, and TypeDoc could auto-generate API documentation from these comments.

Use comprehensive manual documentation instead of TypeDoc auto-generation. Manual documentation provides:

  • Detailed explanations and context
  • Real-world usage examples
  • Best practices and guidance
  • Clear organization and structure
  • Better developer experience

Positive:

  • Quality: Higher quality documentation with context
  • Examples: Real-world usage patterns
  • Guidance: Best practices and recommendations
  • Organization: Logical structure for navigation
  • Experience: Better for developers learning the system

Negative:

  • Maintenance: Manual updates required when code changes
  • Effort: More time to create initially
  • Synchronization Risk: Documentation could become out of sync
  • Consistency: Requires discipline to maintain
  1. TypeDoc Auto-Generation: Evaluated but manual chosen for quality
  2. Hybrid Approach: Considered but manual only chosen for simplicity
  3. Community Documentation: Rejected as it would be inconsistent

Accepted

Tabula Lens needed a modern testing framework that would:

  • Work with TypeScript
  • Support React component testing
  • Provide fast test execution
  • Have good watch mode
  • Integrate well with modern build tools

Use Vitest as the testing framework for both Node and React packages, with:

  • Node environment for backend testing
  • jsdom environment for React testing
  • React Testing Library for component testing
  • Jest-compatible API for familiarity

Positive:

  • Performance: Faster test execution than Jest
  • Modern: Built with modern tooling (Vite)
  • TypeScript: Native TypeScript support
  • React: Excellent React Testing Library integration
  • Familiarity: Jest-compatible API
  • Watch Mode: Fast and reliable watch mode

Negative:

  • Ecosystem: Smaller ecosystem than Jest
  • Maturity: Newer framework with less battle-testing
  • Migration: Requires migration from Jest if used previously
  1. Jest: Considered but Vitest chosen for performance
  2. Mocha: Rejected due to configuration complexity
  3. Ava: Rejected due to smaller ecosystem

ADR-013: Astro with Starlight for Documentation

Section titled “ADR-013: Astro with Starlight for Documentation”

Accepted

Tabula Lens needed a documentation site that would:

  • Be fast and performant
  • Support MDX content
  • Provide excellent navigation
  • Have built-in search
  • Support dark mode
  • Be easy to deploy

Use Astro with Starlight theme for the documentation site, providing:

  • Static site generation for performance
  • MDX support for rich content
  • Built-in navigation and search
  • Dark mode support
  • Responsive design
  • Easy deployment

Positive:

  • Performance: Static site generation for fast loading
  • Developer Experience: Excellent DX with Astro
  • Features: Built-in search, navigation, dark mode
  • Modern: Modern tooling and best practices
  • Customization: Starlight theme is highly customizable
  • Deployment: Easy to deploy to any static host

Negative:

  • Learning Curve: Team needs to learn Astro
  • Build Time: Static generation requires build step
  • Dynamic Content: Limited dynamic content support
  • Theme: Customizing Starlight requires understanding
  1. Docusaurus: Considered but Astro chosen for performance
  2. VitePress: Rejected due to less mature theme
  3. Custom Next.js: Rejected as overkill for documentation

Accepted

Tabula Lens deals with database access and sensitive data. Security needed to be a core consideration from the ground up, not an afterthought.

Implement security-first architecture with:

  • Credential isolation (never in frontend)
  • Authentication at API level
  • Authorization enforcement
  • Input validation and sanitization
  • Secure error handling
  • Comprehensive security documentation

Positive:

  • Security: Strong security posture by default
  • Trust: Users can trust the system with sensitive data
  • Compliance: Helps with regulatory compliance (GDPR, SOC 2, etc.)
  • Best Practices: Encourages security best practices
  • Documentation: Comprehensive security guidance

Negative:

  • Complexity: Additional security layers add complexity
  • Performance: Security checks have overhead
  • Configuration: Requires proper security configuration
  • Learning: Users need to understand security model
  1. Security as Add-On: Rejected as security must be core
  2. User-Managed Security: Rejected as it would lead to insecure implementations
  3. Minimal Security: Rejected as insufficient for production use

Accepted

Tabula Lens needed a design system that would be used across:

  • React package components
  • Documentation site
  • Future applications

A decision needed to be made about where to store the design system source of truth.

Use the React package CSS files as the source of truth for the design system. The React package CSS files (variables.css, global.css) contain all design tokens and styles.

Positive:

  • Single Source: One source of truth for design tokens
  • Consistency: Ensures consistency across all uses
  • Simplicity: No additional files or directories needed
  • Maintenance: Design tokens updated in one place
  • Package-Centric: Design system lives with the components

Negative:

  • Coupling: Documentation site depends on React package
  • Access: Design tokens not accessible without package
  • Updates: Documentation site needs React package updates
  • Separation: Design system not clearly separated
  1. Shared styles/ Directory: Rejected as it would create duplication
  2. Separate Design Package: Rejected as overkill for current needs
  3. Documentation-Only: Rejected as React components need the styles

ADR-018: Error Handling with TabulaLensError

Section titled “ADR-018: Error Handling with TabulaLensError”

Accepted

Tabula Lens needed a consistent error handling approach that would:

  • Provide clear error messages
  • Include error context
  • Support error codes
  • Enable proper error handling in applications
  • Distinguish between different error types

Implement a custom TabulaLensError class with:

  • Error codes for different error types
  • Detailed error messages
  • Error context and metadata
  • Stack trace preservation
  • Consistent error structure

Positive:

  • Consistency: Consistent error structure across the system
  • Debugging: Better error information for debugging
  • Handling: Easier to handle different error types
  • Context: Error context helps understand issues
  • Codes: Error codes enable programmatic handling

Negative:

  • Complexity: Custom error class adds complexity
  • Learning: Users need to understand error structure
  • Maintenance: Error codes and messages need maintenance
  1. Standard Error Objects: Rejected as insufficient context
  2. Third-Party Error Library: Rejected as overkill
  3. No Structured Errors: Rejected as it would make handling difficult

When creating new ADRs, use this template:

## ADR-XXX: [Decision Title]
### Status
[Proposed | Accepted | Deprecated | Superseded]
### Context
[Describe the context and problem statement]
### Decision
[Describe the decision that was made]
### Consequences
[Describe the consequences, both positive and negative]
### Alternatives Considered
[List and describe alternatives that were considered]

To contribute a new ADR:

  1. Create a new ADR using the template above
  2. Discuss with the team
  3. Update the status to “Accepted” once approved
  4. Reference the ADR in relevant code and documentation
  5. Review and update ADRs as the system evolves
  • Review existing ADRs to understand architectural decisions
  • Propose new ADRs for significant architectural changes
  • Update existing ADRs as the system evolves
  • Reference ADRs in code comments and documentation
  • Use ADRs to guide future architectural decisions