Skip to content

refactor: rewrite app with chi and sqlc#10

Merged
steveiliop56 merged 10 commits into
mainfrom
refactor/rewrite
Dec 14, 2025
Merged

refactor: rewrite app with chi and sqlc#10
steveiliop56 merged 10 commits into
mainfrom
refactor/rewrite

Conversation

@steveiliop56

@steveiliop56 steveiliop56 commented Dec 14, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added health check endpoint for service monitoring
    • Implemented rate limiting for API requests with configurable thresholds
    • Instance tracking with heartbeat registration and retrieval endpoints
  • Removals

    • Removed analytics viewer dashboard
  • Chores

    • Updated environment variable configuration (DB_PATH renamed to DATABASE_PATH)
    • Streamlined backend service architecture

✏️ Tip: You can customize this high-level summary in your review settings.

@steveiliop56 steveiliop56 changed the title Refactor/rewrite refactor: rewrite app with chi and sqlc Dec 14, 2025
@steveiliop56

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 14, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Dec 14, 2025

Copy link
Copy Markdown

Warning

Rate limit exceeded

@steveiliop56 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc969b and 984377c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • cache.go (1 hunks)
  • go.mod (1 hunks)
  • instances_handler.go (1 hunks)
  • main.go (1 hunks)
  • rate_limiter.go (1 hunks)

Walkthrough

This pull request executes a major architectural migration from Gin+GORM+Zerolog to Chi+SQLc+slog. The analytics viewer component is removed entirely. Internal service layers (database, cache, rate limit middleware) are replaced with direct implementations. Database access transitions from ORM-based to SQLc-generated query methods. Configuration and environment variables are streamlined, with DB_PATH renamed to DATABASE_PATH. The Dockerfile and Docker Compose configurations are updated to reflect the new stack.

Changes

Cohort / File(s) Summary
Configuration & Environment
.gitignore, .vscode/launch.json, docker-compose.dev.yml, README.md
Updated to reflect DATABASE_PATH env var (was DB_PATH), removed LOG_LEVEL debug entry from analytics service, disabled analytics-dashboard service, adjusted .gitignore to ignore data directory and renamed section header, updated documentation with environment variable table.
Go Module Dependencies
go.mod
Replaced Gin+CORS+GORM+Zerolog stack with Chi+Chi-CORS+SQLc+slog; swapped SQLite driver from glebarez/sqlite to modernc.org/sqlite; removed numerous indirect dependencies tied to Gin ecosystem; updated transitive dependency versions.
Docker Build Configuration
Dockerfile
Modified COPY instructions to include cache.go, health_handler.go, instances_handler.go, rate_limiter.go, and database directory; removed internal directory from build context.
Database Layer — SQLc Code Generation
schema.sql, query.sql, sqlc.yaml, database/queries/db.go, database/queries/models.go, database/queries/query.sql.go
Added SQLc-generated query wrapper, models, and CRUD methods for instances table (CreateInstance, UpdateInstance, DeleteInstance, GetInstance, GetAllInstances, DeleteOldInstances); defined schema with uuid (TEXT PRIMARY KEY), version (TEXT), last_seen (INTEGER).
Core Infrastructure
cache.go, rate_limiter.go
Introduced TTL-based in-memory Cache with background cleanup goroutine; added RateLimiter middleware with per-client usage tracking via cache, client IP detection (supporting CF-Connecting-IP and X-Forwarded-For), and rate limit headers (x-ratelimit-*).
HTTP Handlers
health_handler.go, instances_handler.go
Created health check endpoint returning {"status": "200", "message": "up and running"}; created instances handlers for GetInstances (retrieves all) and Heartbeat (creates or updates instance with UUID and Version, tracking last_seen timestamp in milliseconds).
Application Entrypoint & Orchestration
main.go
Complete rewrite: replaced Gin+GORM initialization with Chi router and direct sql.Open SQLite connection; added Config struct with Port, Address, RateLimitCount, DatabasePath, TrustedProxies, CORSAllowedOrigins; integrated cache, rate limiter, and sqlc queries; switched logging from Zerolog to slog; implemented background cleanup of old instances; removed old controller/middleware layers.
Removed Internal Architecture
internal/controller/health_controller.go, internal/controller/instances_controller.go, internal/middleware/rate_limit_middleware.go, internal/middleware/zerolog_middleware.go, internal/model/instance_model.go, internal/service/cache_service.go, internal/service/database_service.go, internal/service/migrations/000001_init_sqlite.up.sql, internal/service/migrations/000001_init_sqlite.down.sql
Deleted Gin controller layer (HealthController, InstancesController with RateLimit interface), removed Zerolog and rate limit middleware, removed GORM-based Instance model, removed CacheService and DatabaseService abstractions with GORM/golang-migrate integration, deleted old database migrations.
Viewer / Frontend Removal
viewer/main.ts, viewer/package.json, viewer/tsconfig.json, viewer/README.md, viewer/.gitignore
Removed analytics viewer TypeScript application including instance fetching, formatting, and logging logic; removed Bun package configuration and TypeScript compiler settings; deleted viewer documentation and node_modules ignore entry.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • main.go: Verify complete initialization flow (config parsing via Viper, database connection, cache/rate limiter setup, Chi router configuration, handler registration) and ensure no orphaned references to removed services
  • Rate limiter logic: Confirm client IP detection handles CF-Connecting-IP, X-Forwarded-For, and trusted proxies correctly; validate rate limit cache key generation and TTL (12 hours) appropriateness
  • Cache implementation: Review background cleanup goroutine (24-hour ticker), concurrency safety via mutex, and TTL expiration logic
  • SQLc query methods: Verify query generation accuracy, parameter binding, result scanning for Instance type, and error handling (especially DeleteOldInstances :many return behavior)
  • Instances handler: Validate heartbeat logic for create-vs-update branching, LastSeen timestamp as UnixMilli, and response status codes
  • Dockerfile changes: Confirm new file copy list doesn't omit critical source files and that the build produces expected binary
  • Dependency migration: Spot-check go.mod for transitive dependency compatibility, especially modernc.org/sqlite version and Chi-related packages

🐰 A Tale of Rebirth

Once a Gin-based dreamer with GORM's ORM delight,
Now Chi routes whisper, SQLc queries bite.
The viewer took flight, the cache breathes anew,
Rate limits hold firm—architecture gleams true! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: rewrite app with chi and sqlc' accurately describes the main changes: a comprehensive refactor replacing Gin with Chi router and adding sqlc for database query generation.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Dockerfile (1)

35-35: Fix environment variable name inconsistency.

The Dockerfile uses DB_PATH, but the application has been refactored to use DATABASE_PATH (as seen in docker-compose.dev.yml line 9 and .vscode/launch.json line 11). This inconsistency will prevent the containerized application from finding the database.

Apply this diff to fix the environment variable name:

-ENV DB_PATH=/data/analytics.db
+ENV DATABASE_PATH=/data/analytics.db
🧹 Nitpick comments (9)
Dockerfile (1)

43-43: Remove unused Gin framework environment variable.

The application has been refactored from Gin to Chi router. The GIN_MODE environment variable is no longer relevant and should be removed.

Apply this diff to remove the unused environment variable:

-ENV GIN_MODE=release
-
query.sql (1)

15-18: Consider uppercase SQL keywords for consistency.

While SQL is case-insensitive and set works correctly, using uppercase SET follows the conventional style seen in your other queries and improves readability.

Apply this diff if you'd like to standardize:

 -- name: UpdateInstance :exec
 UPDATE instances
-set last_seen = ?
+SET last_seen = ?
 WHERE uuid = ?;
health_handler.go (1)

15-21: Redundant WriteHeader call before render.JSON.

render.JSON internally sets the Content-Type header and writes the response. Calling w.WriteHeader(http.StatusOK) beforehand is unnecessary for 200 OK (the default) and could cause issues if headers need to be modified by render. This pattern is inconsistent with how other handlers in instances_handler.go use it (where WriteHeader is called for non-200 statuses).

 func (h *HealthHandler) health(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusOK)
 	render.JSON(w, r, map[string]string{
 		"status":  "200",
 		"message": "up and running",
 	})
 }
cache.go (1)

64-68: Nitpick: Remove unnecessary capacity hint.

The 0 capacity hint in make(map[string]cacheField, 0) is redundant.

 func (c *Cache) Flush() {
 	c.mutex.Lock()
 	defer c.mutex.Unlock()
-	c.cache = make(map[string]cacheField, 0)
+	c.cache = make(map[string]cacheField)
 }
instances_handler.go (1)

41-56: Consider validating UUID and Version are non-empty.

The heartbeat payload is decoded but there's no validation that UUID and Version are non-empty strings. Empty values would be persisted to the database.

 	err := render.DecodeJSON(r.Body, &heartbeat)
 
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		render.JSON(w, r, map[string]string{
 			"status":  "400",
 			"message": "Invalid request payload",
 		})
 		return
 	}
+
+	if heartbeat.UUID == "" || heartbeat.Version == "" {
+		w.WriteHeader(http.StatusBadRequest)
+		render.JSON(w, r, map[string]string{
+			"status":  "400",
+			"message": "UUID and version are required",
+		})
+		return
+	}
rate_limiter.go (1)

30-34: Performance concern: Global lock serializes all requests.

Using rl.mutex.Lock() for the entire middleware serializes all HTTP requests through the rate limiter, creating a bottleneck. Since the Cache already has its own mutex for thread-safe access, this outer lock appears redundant.

Consider removing the RateLimiter-level mutex since Cache operations are already thread-safe, or use more granular per-IP locking if atomicity between Get and Set is required.

main.go (3)

61-67: Missing error handling for SQL statements.

The PRAGMA and CREATE TABLE executions ignore errors. If WAL mode cannot be enabled or table creation fails, the application continues with a potentially broken state.

-	sqlDb.Exec(`PRAGMA journal_mode=WAL;`)
+	if _, err := sqlDb.Exec(`PRAGMA journal_mode=WAL;`); err != nil {
+		slog.Warn("failed to enable WAL mode", "error", err)
+	}
 
-	sqlDb.Exec(`CREATE TABLE IF NOT EXISTS "instances" (
+	_, err = sqlDb.Exec(`CREATE TABLE IF NOT EXISTS "instances" (
 		"uuid" TEXT NOT NULL PRIMARY KEY,
 		"version" TEXT NOT NULL,
 		"last_seen" INTEGER NOT NULL
-	);`)
+	);`)
+	if err != nil {
+		slog.Error("failed to create instances table", "error", err)
+		os.Exit(1)
+	}

121-129: Misleading variable name: rowsAffected holds deleted instances.

DeleteOldInstances returns []Instance (the deleted records), not a count. The variable name rowsAffected is misleading.

-		rowsAffected, err := queries.DeleteOldInstances(context.Background(), cutoffTime)
+		deletedInstances, err := queries.DeleteOldInstances(context.Background(), cutoffTime)
 
 		if err != nil {
 			slog.Error("failed to clean up old instances: ", "error", err)
 			continue
 		}
 
-		slog.Info("old instances cleaned up", "rows_affected", rowsAffected)
+		slog.Info("old instances cleaned up", "count", len(deletedInstances))

97-111: No graceful shutdown handling.

The server starts without signal handling. On SIGTERM/SIGINT, connections are dropped abruptly and the cleanup goroutine cannot exit cleanly. Consider using signal.NotifyContext and srv.Shutdown.

This is a good-to-have improvement for production deployments to ensure in-flight requests complete and resources are released properly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c11e460 and 6bc969b.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • viewer/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • .gitignore (1 hunks)
  • .vscode/launch.json (1 hunks)
  • Dockerfile (1 hunks)
  • README.md (2 hunks)
  • cache.go (1 hunks)
  • database/queries/db.go (1 hunks)
  • database/queries/models.go (1 hunks)
  • database/queries/query.sql.go (1 hunks)
  • docker-compose.dev.yml (1 hunks)
  • go.mod (1 hunks)
  • health_handler.go (1 hunks)
  • instances_handler.go (1 hunks)
  • internal/controller/health_controller.go (0 hunks)
  • internal/controller/instances_controller.go (0 hunks)
  • internal/middleware/rate_limit_middleware.go (0 hunks)
  • internal/middleware/zerolog_middleware.go (0 hunks)
  • internal/model/instance_model.go (0 hunks)
  • internal/service/cache_service.go (0 hunks)
  • internal/service/database_service.go (0 hunks)
  • internal/service/migrations/000001_init_sqlite.down.sql (0 hunks)
  • internal/service/migrations/000001_init_sqlite.up.sql (0 hunks)
  • main.go (1 hunks)
  • query.sql (1 hunks)
  • rate_limiter.go (1 hunks)
  • schema.sql (1 hunks)
  • sqlc.yaml (1 hunks)
  • viewer/.gitignore (0 hunks)
  • viewer/README.md (0 hunks)
  • viewer/main.ts (0 hunks)
  • viewer/package.json (0 hunks)
  • viewer/tsconfig.json (0 hunks)
💤 Files with no reviewable changes (14)
  • viewer/README.md
  • viewer/main.ts
  • internal/controller/health_controller.go
  • viewer/.gitignore
  • internal/controller/instances_controller.go
  • viewer/package.json
  • internal/model/instance_model.go
  • viewer/tsconfig.json
  • internal/service/migrations/000001_init_sqlite.up.sql
  • internal/middleware/zerolog_middleware.go
  • internal/service/database_service.go
  • internal/middleware/rate_limit_middleware.go
  • internal/service/migrations/000001_init_sqlite.down.sql
  • internal/service/cache_service.go
🧰 Additional context used
🧬 Code graph analysis (4)
instances_handler.go (2)
database/queries/db.go (1)
  • Queries (23-25)
database/queries/query.sql.go (2)
  • CreateInstanceParams (20-24)
  • UpdateInstanceParams (115-118)
rate_limiter.go (1)
cache.go (1)
  • Cache (13-16)
database/queries/query.sql.go (2)
database/queries/db.go (1)
  • Queries (23-25)
database/queries/models.go (1)
  • Instance (7-11)
main.go (5)
database/queries/db.go (2)
  • New (19-21)
  • Queries (23-25)
cache.go (1)
  • NewCache (18-24)
rate_limiter.go (2)
  • NewRateLimiter (23-28)
  • RateLimitConfig (12-15)
instances_handler.go (1)
  • NewInstancesHandler (15-19)
health_handler.go (1)
  • NewHealthHandler (11-13)
🪛 LanguageTool
README.md

[grammar] ~7-~7: Ensure spelling is correct
Context: ...tains the UUID generated by Tinyauth on start up and the version information. The server...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (12)
go.mod (1)

5-12: No sqlc dependency is required in go.mod.

The generated sqlc code uses only the standard library (context and database/sql), not any sqlc runtime packages. sqlc is a code-generation tool that produces self-contained Go code—it is not a runtime dependency and should not appear in go.mod.

.vscode/launch.json (1)

11-11: LGTM!

The environment variable rename from DB_PATH to DATABASE_PATH aligns with the broader refactoring effort and is consistent with the docker-compose configuration.

.gitignore (1)

2-11: LGTM!

The changes appropriately ignore the entire data directory (more robust than a specific file) and include the compiled binary in the build output section.

Dockerfile (1)

13-18: LGTM!

The expanded build context correctly includes all new source files required for the Chi+SQLc architecture.

sqlc.yaml (1)

1-14: LGTM!

The sqlc configuration is well-structured. The column override for instances.id to exclude it from JSON serialization and the UUID rename both follow Go conventions.

docker-compose.dev.yml (1)

21-37: LGTM!

Commenting out the analytics-dashboard service aligns with the PR's architectural shift to a leaner backend-focused deployment.

database/queries/models.go (1)

1-11: LGTM!

This is sqlc-generated code that correctly represents the instances table schema. The Instance struct appropriately maps the database columns.

schema.sql (1)

1-5: LGTM!

The instances table schema is clean and appropriate. Using TEXT for UUID and INTEGER for Unix timestamp are both standard SQLite practices. The IF NOT EXISTS clause ensures idempotent execution.

query.sql (1)

1-13: LGTM!

All queries are well-formed, use parameterized placeholders for security, and follow SQLite best practices. The DeleteOldInstances query with RETURNING * is particularly good for tracking which instances were pruned.

Also applies to: 20-27

database/queries/db.go (1)

1-31: LGTM!

This is standard sqlc-generated code providing a clean abstraction over database operations with transaction support via WithTx.

database/queries/query.sql.go (1)

1-123: LGTM!

This is well-structured sqlc-generated code with proper resource management (deferred rows.Close()) and error handling patterns.

rate_limiter.go (1)

72-77: Security consideration: CF-Connecting-IP is trusted unconditionally.

This header is only reliable when the request comes through Cloudflare. If the server is accessed directly (bypassing Cloudflare), clients can spoof this header. Consider validating that RemoteAddr is a known Cloudflare IP range before trusting this header.

Is this service always deployed behind Cloudflare? If not, an attacker could bypass rate limiting by setting a fake CF-Connecting-IP header.

Comment thread cache.go
Comment thread go.mod Outdated
Comment thread instances_handler.go
Comment thread main.go Outdated
Comment thread rate_limiter.go Outdated
Comment thread rate_limiter.go Outdated
Comment thread README.md
@steveiliop56 steveiliop56 merged commit 7649d53 into main Dec 14, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant