Appearance
Architecture
This document describes the internal architecture of Faucet for contributors and LLM agents working on the codebase.
Project Structure
faucet/
├── cmd/faucet/
│ ├── main.go # Entry point
│ └── cli/
│ ├── root.go # Root command, flag setup, config init
│ ├── serve.go # HTTP server startup
│ ├── db.go # Database service management
│ ├── admin.go # Admin user management
│ ├── key.go # API key management
│ ├── role.go # RBAC role management
│ ├── openapi.go # OpenAPI spec generation
│ ├── mcp.go # MCP server startup
│ ├── benchmark.go # Load testing
│ ├── config_cmd.go # Config file init/show
│ ├── version.go # Version info
│ └── helpers.go # Shared config store/registry helpers
├── internal/
│ ├── config/
│ │ ├── store.go # SQLite config store (CRUD for all entities)
│ │ ├── store_test.go # Config store tests
│ │ ├── migrations.go # Schema migrations
│ │ ├── yaml.go # YAML config support
│ │ └── errors.go # Sentinel errors (ErrNotFound, etc.)
│ ├── connector/
│ │ ├── connector.go # Connector interface + request types
│ │ ├── registry.go # Connector factory/registry
│ │ ├── postgres/ # PostgreSQL connector
│ │ ├── mysql/ # MySQL connector
│ │ ├── mssql/ # SQL Server connector
│ │ ├── snowflake/ # Snowflake connector
│ │ └── sqlite/ # SQLite connector
│ ├── handler/
│ │ ├── system.go # System API handlers (services, roles, admins, keys)
│ │ ├── table.go # Table CRUD handlers
│ │ ├── schema.go # Schema introspection/DDL handlers
│ │ ├── proc.go # Stored procedure handlers
│ │ ├── openapi.go # OpenAPI spec endpoint
│ │ └── helpers.go # JSON read/write utilities
│ ├── mcp/
│ │ ├── server.go # MCP server (stdio + HTTP)
│ │ ├── handler.go # JSON-RPC message dispatch
│ │ ├── tools.go # MCP tool definitions
│ │ └── resources.go # MCP resource definitions
│ ├── model/
│ │ ├── service.go # ServiceConfig, PoolConfig
│ │ ├── admin.go # Admin model
│ │ ├── role.go # Role, RoleAccess, Filter, verb constants
│ │ ├── apikey.go # APIKey model
│ │ ├── schema.go # Schema, TableSchema, Column, ForeignKey, etc.
│ │ └── response.go # ListResponse, ResponseMeta envelope
│ ├── openapi/
│ │ ├── generator.go # OpenAPI 3.1 spec generation from schemas
│ │ └── types.go # DB type → OpenAPI type mapping
│ ├── query/
│ │ ├── parser.go # Filter expression parser
│ │ ├── builder.go # SQL query builder
│ │ └── sanitizer.go # Input sanitization
│ ├── server/
│ │ ├── server.go # HTTP server, Chi router, route setup
│ │ └── middleware/
│ │ ├── auth.go # Authentication middleware (JWT + API key)
│ │ ├── logging.go # Structured request logging
│ │ ├── ratelimit.go # Token bucket rate limiting
│ │ └── requestid.go # X-Request-ID header injection
│ ├── service/
│ │ ├── auth.go # AuthService (JWT issue/validate, API key lookup)
│ │ └── auth_test.go # Auth service tests
│ └── ui/
│ ├── embed.go # go:embed directive for dist/
│ └── dist/ # Built Preact UI assets
├── ui/ # Preact UI source code
│ ├── src/
│ ├── package.json
│ └── vite.config.ts
├── docs/ # Documentation
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── .goreleaser.ymlRequest Lifecycle
Every API request follows this path:
HTTP Request → Chi Router
↓
Global Middleware Stack (in order):
1. RequestID — generates UUID, sets X-Request-ID header
2. Logger — structured request logging via slog
3. Recoverer — panic recovery
4. RealIP — extract real client IP
5. CORS — cross-origin resource sharing
6. Compress — gzip response compression
↓
Route Matching:
/healthz, /readyz → Health check handlers (no auth)
/openapi.json → Combined OpenAPI spec (no auth)
/api/v1/system/* → System handlers (admin auth required)
/api/v1/{service}/* → Service handlers (API key or JWT auth)
/admin, /setup, etc. → Embedded UI (SPA, no auth — UI handles login)
↓
Authentication Middleware (on protected routes):
Extract API key from X-API-Key header
OR extract JWT from Authorization: Bearer header
→ Creates Principal context value
↓
Handler:
1. Parse query parameters (filter, fields, order, limit, offset)
2. Resolve service name → connector from registry
3. Introspect schema (cached)
4. Build parameterized SQL via connector's query builder
5. Execute query via sqlx with connection pooling
6. Format response as JSON envelope {resource: [...], meta: {...}}
↓
ResponseKey Abstractions
Connector Interface
The central abstraction. Every database implements this interface:
go
type Connector interface {
Connect(cfg ConnectionConfig) error
Disconnect() error
Ping(ctx context.Context) error
DB() *sqlx.DB
IntrospectSchema(ctx context.Context) (*model.Schema, error)
IntrospectTable(ctx context.Context, tableName string) (*model.TableSchema, error)
GetTableNames(ctx context.Context) ([]string, error)
GetStoredProcedures(ctx context.Context) ([]model.StoredProcedure, error)
BuildSelect(ctx context.Context, req SelectRequest) (string, []interface{}, error)
BuildInsert(ctx context.Context, req InsertRequest) (string, []interface{}, error)
BuildUpdate(ctx context.Context, req UpdateRequest) (string, []interface{}, error)
BuildDelete(ctx context.Context, req DeleteRequest) (string, []interface{}, error)
BuildCount(ctx context.Context, req CountRequest) (string, []interface{}, error)
CreateTable(ctx context.Context, def model.TableSchema) error
AlterTable(ctx context.Context, tableName string, changes []SchemaChange) error
DropTable(ctx context.Context, tableName string) error
CallProcedure(ctx context.Context, name string, params map[string]interface{}) ([]map[string]interface{}, error)
DriverName() string
QuoteIdentifier(name string) string
SupportsReturning() bool
SupportsUpsert() bool
ParameterPlaceholder(index int) string
}Each connector (postgres, mysql, mssql, snowflake, sqlite) implements dialect-specific SQL generation. The handlers are database-agnostic — they only interact through this interface.
Registry
The connector.Registry is a thread-safe map of service name → active Connector. It also holds driver factories for lazy instantiation.
Config Store
config.Store wraps an embedded SQLite database that persists:
- Database service configurations (name, driver, DSN, pool settings)
- Admin accounts (email, bcrypt password hash)
- RBAC roles and access rules
- API keys (SHA-256 hashed)
All CLI commands and system API handlers share the same store.
Auth Service
service.AuthService handles:
- JWT token issuance and validation
- API key lookup (hash the incoming key, query store)
- Principal creation from authenticated credentials
Adding a New Database Connector
- Create
internal/connector/newdb/connector.goimplementing theConnectorinterface - Add introspection queries in
internal/connector/newdb/introspect.go - Add query builder in
internal/connector/newdb/query_builder.go - Register the driver in the registry setup (in
serve.go,mcp.go,helpers.go) - Add the driver to
go.moddependencies - Add tests
Configuration Precedence
- CLI flags (highest priority)
- Environment variables (
FAUCET_*prefix) - Config file (
faucet.yamlin cwd or~/.faucet/) - Defaults
Viper handles the merging. The config store (SQLite) is the persistent state for services, roles, keys, and admins.
Concurrency Model
- The HTTP server uses Go's
net/httpwith Chi router — each request gets its own goroutine - Each database service has its own
sql.DBconnection pool with configurable limits - The connector registry uses
sync.RWMutexfor thread-safe access - The config store uses SQLite with
MaxOpenConns=1(SQLite limitation for writes) - Graceful shutdown via
signal.NotifyContextfor SIGINT/SIGTERM
Security
- All filter values are parameterized — never string-interpolated into SQL
- API keys are stored as SHA-256 hashes
- Admin passwords are hashed (SHA-256 currently, bcrypt planned)
- DSN values are never exposed in API list responses
- Rate limiting middleware available per-key and per-role
- RBAC with row-level security filters