This document provides comprehensive information about the DefiFundr database architecture, schema, migration workflows, and query patterns.
- DefiFundr Database Guide
DefiFundr uses PostgreSQL as its primary database. The database stores user information, authentication data, transactions, and other application state. We use the following tools to manage our database:
- PostgreSQL - Relational database
- Goose - Database migration management
- SQLC - SQL compiler for Go
- DBML - Database markup language for documentation
The database schema includes these primary tables:
users- User accounts and profile informationsessions- User authentication sessionskyc- Know Your Customer verification datauser_device- User device informationotp- One-time password verification recordstransactions- Financial transaction records
The complete schema is defined in migration files located in db/migrations/. You can also view the schema in DBML format at docs/db_diagram/db.dbml.
To generate updated schema documentation:
# Generate SQL schema from DBML
make db_schema
# Generate DB documentation
make db_docsWe use Goose for database migrations. Migration files are stored in db/migrations/ and follow a sequential numbering pattern.
To create a new migration:
make migrate-create
# Enter a descriptive name when promptedThis creates a new SQL migration file with up and down migrations.
To apply pending migrations:
# Apply all pending migrations
make migrate-up
# Apply only the next pending migration
make migrate-up-oneTo revert migrations:
# Revert the most recent migration
make migrate-down
# Revert the most recent migration only
make migrate-down-one
# Revert all migrations
make migrate-resetTo see the current migration status:
make migrate-statusWe use SQLC to generate type-safe Go code from SQL queries. SQL queries are defined in db/query/ directory.
- Define your SQL queries in files under
db/query/ - Run the SQL code generator:
make sqlc- Use the generated Go code in your repositories
SQLC generates:
- Strong types for rows and parameters
- Idiomatic Go functions for each query
- Interface definitions for mocking
Here's an example SQL query from db/query/users.sql:
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY name;
-- name: CreateUser :one
INSERT INTO users (
name, email, hashed_password, created_at
) VALUES (
$1, $2, $3, $4
)
RETURNING *;We implement the repository pattern using interfaces defined in internal/core/ports/repository.go and implemented in internal/adapters/repositories/.
For operations that require multiple queries in a transaction, use the Store interface that provides transaction support.
Example:
err := store.ExecTx(ctx, func(q *Queries) error {
// Execute multiple queries within a transaction
return nil
})For development and testing, you can seed the database with sample data:
make seedThe seeding logic is defined in cmd/seed/main.go.
-
Migration Safety:
- Always include both "up" and "down" migrations
- Test migrations in development before applying to production
- Avoid modifying existing migrations after they've been applied
-
Query Organization:
- Group related queries in the same .sql file
- Use clear, descriptive query names
- Document complex queries with comments
-
Performance:
- Add appropriate indexes for frequently queried columns
- Use EXPLAIN ANALYZE to check query performance
- Implement pagination for large result sets using the utilities in
pkg/pagination
-
Testing:
- Write tests for repository implementations
- Use the mock repository for service-level testing
-
Schema Changes:
- Document significant schema changes in PR descriptions
- Consider data migration needs when changing schemas
- Use database constraints to enforce data integrity