Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
e7b8a67
fix: normalize polar_whs_ prefix to whsec_ for Svix verification
dasturchioka Apr 14, 2026
f993acf
fix: add base64 padding to polar_whs_ secret before Svix init
dasturchioka Apr 14, 2026
ae1c162
fix: always sync webhook secrets from env on startup
dasturchioka Apr 14, 2026
90d98a0
refactor: DB is source of truth for webhook secrets
dasturchioka Apr 14, 2026
77fb1cc
refactor: remove webhook secret env vars
dasturchioka Apr 14, 2026
6c31fd2
fix(polar): correct webhook secret normalization and add deploy teleg…
dasturchioka Apr 15, 2026
31a6c1b
feat(api): async job platform — remove AI/webhook work from request path
dasturchioka Apr 15, 2026
d65bab4
fix(lint): resolve all golangci-lint, gosec, staticcheck violations
dasturchioka Apr 15, 2026
efea395
fix(test): restore synchronous path in AdminSyncPolarStoreProductsHan…
dasturchioka Apr 15, 2026
a235f46
fix(lint): goimports formatting and remove unused queuePolarSyncResponse
dasturchioka Apr 15, 2026
7898dcf
chore: ignore .worktrees/ directory
dasturchioka Apr 15, 2026
72ac3b5
feat(api): scale async voice jobs with shared storage workers
dasturchioka Apr 15, 2026
55a19ce
refactor(api): lock async voice jobs to local temp storage
dasturchioka Apr 15, 2026
e3e5f21
feat(api): queue voice jobs with local temp audio
dasturchioka Apr 15, 2026
d2620f0
feat(api): same-host async voice worker lifecycle
dasturchioka Apr 15, 2026
0de41bf
fix(api): align same-host worker limits and boot
dasturchioka Apr 15, 2026
87a0fbb
refactor(api): remove legacy async voice s3 storage
dasturchioka Apr 15, 2026
0c9fbde
refactor(api): remove dead S3 config and simplify object store to loc…
dasturchioka Apr 15, 2026
f091df7
fix(lint): goimports formatting and gosec G304/G122 in temp audio cle…
dasturchioka Apr 15, 2026
7a99f04
feat: live log streaming via SSE for admin panel
dasturchioka Apr 16, 2026
cba1e48
fix: disable write deadline for SSE log stream connection
dasturchioka Apr 16, 2026
9ceb0d9
fix: goimports formatting in LogBroadcaster struct fields
dasturchioka Apr 16, 2026
72ce26f
fix: gofmt-correct struct field alignment in LogBroadcaster
dasturchioka Apr 16, 2026
1f71184
fix: add minio-go dependency for S3 storage backend
dasturchioka Apr 16, 2026
f1a1455
fix: add minio-go dependency for S3 storage backend
dasturchioka Apr 16, 2026
4ff28e5
Merge branch 'staging' of github.com:novda-union/numex-api into staging
dasturchioka Apr 16, 2026
e69a006
fix: fail CI deploy on build error using set -e
dasturchioka Apr 16, 2026
bb5b836
fix: add missing S3Config struct to s3.go
dasturchioka Apr 16, 2026
0cc72ec
fix: fresh start
dasturchioka Apr 17, 2026
6e7dfc4
fix: start fresh
dasturchioka Apr 18, 2026
8bdd25f
feat: add EUR currency seed, add onboarding context columns to users
dasturchioka Apr 18, 2026
f62b83f
fix: add ALTER TABLE migration statements and CHECK constraints for o…
dasturchioka Apr 18, 2026
a92dfe7
feat: add PATCH /user/onboarding-context endpoint
dasturchioka Apr 18, 2026
3138065
fix: add deleted_at guard, updated_at, fix op name in onboarding-cont…
dasturchioka Apr 18, 2026
26bfc82
fix: removed jwt from currency handler
dasturchioka Apr 19, 2026
64afb21
chore(dev): add local API setup
dasturchioka Apr 27, 2026
a89ec33
fix(dev): use local env for make init
dasturchioka Apr 27, 2026
d13b2dc
Update polar.go
dasturchioka Apr 28, 2026
28ed0ef
fix: prevent debt fallback in voice parsing
dasturchioka May 1, 2026
578c8ef
fix: add transaction reprocess endpoint
dasturchioka May 1, 2026
08f1604
fix: use polar webhook env fallback
dasturchioka May 2, 2026
1c0b374
fix: satisfy provider limits vet check
dasturchioka May 2, 2026
a517cf9
feat(account): delete users immediately
dasturchioka May 3, 2026
0ca0443
fix(clients): fix errcheck lint failure and clean up polar client
bclayn24 May 3, 2026
f66c849
fix: removed jobs & async queue system
dasturchioka May 5, 2026
fef46ad
feat(api): secure debt bundle writes
dasturchioka May 6, 2026
0f0b0d4
currency state [15:37]
bclayn24 May 6, 2026
89128a1
Error message fix
bclayn24 May 6, 2026
680aaa5
fix(make): small syntax problem is fixed
dasturchioka May 6, 2026
e1f0ed1
fix: prefill polar checkout email and harden insight json generation
dasturchioka May 7, 2026
a1c0fb1
Merge pull request #13 from novda-union/polar-confirmation-insight-re…
dasturchioka May 7, 2026
08eb7fd
fix: duplication of subscription rows
dasturchioka May 7, 2026
126cb33
fix: webhook create 503
dasturchioka May 7, 2026
1fdeba2
fix: insights
dasturchioka May 7, 2026
5cd0a87
fix
dasturchioka May 7, 2026
175e77d
formatted
dasturchioka May 8, 2026
49573cd
reversed before plan
dasturchioka May 8, 2026
143a876
feat: sync Polar trial periods
dasturchioka May 8, 2026
42a01b6
fix: bump Go security patches
dasturchioka May 8, 2026
45ea12f
fix(api): harden start fresh reset
dasturchioka May 17, 2026
78d62c2
feat(api): deleted unused, blocking test for now
dasturchioka May 17, 2026
c9adac3
fix(api): removed deprecated checks:
dasturchioka May 17, 2026
888b8cd
test(api): align Gemini retry coverage
dasturchioka May 17, 2026
d49c083
fix(ai): upgraded prompt for better debt and category handling
dasturchioka May 18, 2026
81f6e3a
fix(context): giving more context to parser AI properly
dasturchioka May 18, 2026
870f3c7
fix(quality)
dasturchioka May 18, 2026
64855a5
fix
dasturchioka May 21, 2026
fd716c6
removed hardcoded geminiSystemInstruction
dasturchioka May 21, 2026
84fa641
fix: added verbose body logging, malformed AI responses
dasturchioka May 21, 2026
4a31fa3
chore: log all request bodies when verbose
dasturchioka May 23, 2026
720172e
fix: remove unused helpers
dasturchioka May 23, 2026
ce4fa41
fixed last vuln
dasturchioka May 23, 2026
31e09fc
fix(debts): verify repayments in app
dasturchioka May 26, 2026
f0e2a5b
feat(sync): store balances as private data
dasturchioka May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,3 @@ POLAR_WEBHOOK_SECRET=
POLAR_MODE=
POLAR_SUCCESS_URL=
ADMIN_EMAIL=
ADMIN_PASSWORD_HASH=
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,21 @@ jobs:
needs: ci
if: github.ref == 'refs/heads/staging' && github.event_name == 'push'
runs-on: ubuntu-latest
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
PROCESS_NAME: Numex API - Staging
steps:
- uses: actions/checkout@v4

- name: Notify staging deploying
if: ${{ env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Deploying"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"

- name: Copy source to server
uses: appleboy/scp-action@v0.1.7
with:
Expand All @@ -68,17 +80,46 @@ jobs:
username: root
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
set -e
docker compose -f /opt/numex/deploy/docker-compose.yml build numex-api-staging
docker compose -f /opt/numex/deploy/docker-compose.yml up -d numex-api-staging
docker image prune -f

- name: Notify staging live
if: ${{ success() && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Live"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"

- name: Notify staging failed
if: ${{ failure() && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Failed"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"

deploy-prod:
needs: ci
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
PROCESS_NAME: Numex API - Production
steps:
- uses: actions/checkout@v4

- name: Notify prod deploying
if: ${{ env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Deploying"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"

- name: Copy source to server
uses: appleboy/scp-action@v0.1.7
with:
Expand All @@ -96,6 +137,23 @@ jobs:
username: root
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
set -e
docker compose -f /opt/numex/deploy/docker-compose.yml build numex-api-prod
docker compose -f /opt/numex/deploy/docker-compose.yml up -d numex-api-prod
docker image prune -f

- name: Notify prod live
if: ${{ success() && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Live"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"

- name: Notify prod failed
if: ${{ failure() && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' }}
run: |
MESSAGE="${PROCESS_NAME}"$'\n'"Failed"
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${MESSAGE}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.env
.env.local
adminpasswd
.worktrees/
tmp-api-dev*.log
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1

FROM golang:1.25-alpine AS verify
FROM golang:1.25.10-alpine AS verify
WORKDIR /app

COPY go.mod go.sum ./
Expand All @@ -9,7 +9,7 @@ RUN go mod download
COPY . .
RUN go vet ./... && go build ./cmd/api

FROM golang:1.25-alpine AS builder
FROM golang:1.25.10-alpine AS builder
WORKDIR /app

COPY go.mod go.sum ./
Expand Down
19 changes: 11 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
DB_CONTAINER=numex_db
ENV_FILE ?= .env.local
DB_CONTAINER ?= numex_db
DB_USER ?= numex
DB_NAME ?= numex

ifeq ($(OS),Windows_NT)
WAIT_CMD = @powershell -Command "Start-Sleep 2; while (!(docker exec $(DB_CONTAINER) pg_isready -U numex 2>$$null)) { Start-Sleep 1 }"
WAIT_CMD = @powershell -NoProfile -Command 'Start-Sleep 2; do { docker exec $(DB_CONTAINER) pg_isready -U $(DB_USER) -d $(DB_NAME) 2>$$null; if ($$LASTEXITCODE -ne 0) { Start-Sleep 1 } } until ($$LASTEXITCODE -eq 0)'
else
WAIT_CMD = @sleep 2 && until docker exec $(DB_CONTAINER) pg_isready -U numex; do sleep 1; done
WAIT_CMD = @sleep 2 && until docker exec $(DB_CONTAINER) pg_isready -U $(DB_USER) -d $(DB_NAME); do sleep 1; done
endif

ifeq ($(OS),Windows_NT)
Expand All @@ -14,17 +17,17 @@ endif

init:
@echo Starting containers...
docker compose up -d
docker compose --env-file $(ENV_FILE) up -d --wait --wait-timeout 60
@echo Giving time for PostgreSQL to start...
$(WAIT_CMD)
@echo Injecting schema...
docker exec -i $(DB_CONTAINER) psql -U numex -d numex < internal/db/schema.sql
docker exec -i $(DB_CONTAINER) psql -U $(DB_USER) -d $(DB_NAME) < internal/db/schema.sql
@echo Seeding data...
docker exec -i $(DB_CONTAINER) psql -U numex -d numex < internal/db/data.sql
docker exec -i $(DB_CONTAINER) psql -U $(DB_USER) -d $(DB_NAME) < internal/db/data.sql
@echo The database was initialized successfully.

down:
docker compose down -v
docker compose --env-file $(ENV_FILE) down -v
@echo Containers and data have been deleted.

run:
Expand Down Expand Up @@ -75,7 +78,7 @@ endif
psql "$(DATABASE_URL)" -q; \
else \
{ printf '\\set h %s\n' "$$HASH"; printf "UPDATE app_config SET value = to_json(:'h'::text) WHERE key = 'admin_password_hash';\n"; } | \
docker exec -i $(DB_CONTAINER) psql -U numex -d numex -q; \
docker exec -i $(DB_CONTAINER) psql -U $(DB_USER) -d $(DB_NAME) -q; \
fi && \
echo "Admin password updated successfully."

Expand Down
39 changes: 21 additions & 18 deletions cmd/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"

"numex-api/internal/broadcast"
"numex-api/internal/cache"
"numex-api/internal/clients"
"numex-api/internal/config"
Expand All @@ -20,7 +23,6 @@ import (
"numex-api/internal/handlers"
"numex-api/internal/middlewares"
"numex-api/internal/services"
"numex-api/internal/workers"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -78,6 +80,10 @@ func run() error {
}
emailService := clients.NewEmailService(configCache)

broadcaster := broadcast.NewLogBroadcaster(500)
logWriter := io.MultiWriter(os.Stdout, broadcaster)
log.SetOutput(logWriter)

s := handlers.Server{
DB: pool,
Queries: q,
Expand All @@ -88,34 +94,24 @@ func run() error {
Payme: paymeClient,
Polar: polarClient,
Email: emailService,
Broadcaster: broadcaster,
}

if polarClient != nil && strings.Trim(strings.TrimSpace(configCache.GetString("polar_enabled", "false")), `"`) == "true" {
summary, err := handlers.SyncPolarStoreProducts(context.Background(), polarClient, q)
if err != nil {
if _, err := handlers.SyncPolarStoreProducts(context.Background(), polarClient, q); err != nil {
log.Printf("polar store products: startup sync failed: %v", err)
} else {
log.Printf(
"polar store products: startup sync complete created=%d updated=%d deactivated=%d skipped=%d",
summary.Created,
summary.Updated,
summary.Deactivated,
summary.Skipped,
)
log.Printf("polar store products: startup sync completed")
}
}

e := setupEcho(ipExtractor)
e := setupEcho(ipExtractor, logWriter)
handlers.Handlers(e, &s)

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

var wg sync.WaitGroup
billingWorker := workers.NewBillingWorker(pool, paymeClient, emailService)
billingWorker.Start(ctx, time.Minute, &wg)
downgradeCleanupWorker := workers.NewDowngradeCleanupWorker(pool)
downgradeCleanupWorker.Start(ctx, time.Minute, &wg)
configCache.StartAutoRefresh(ctx, 5*time.Minute, &wg)
services.StartRateFetcher(ctx, q, &wg)
services.StartIdempotencyCleanup(ctx, q, &wg)
Expand All @@ -125,7 +121,7 @@ func run() error {
return err
}

func setupEcho(ipExtractor func(*http.Request) string) *echo.Echo {
func setupEcho(ipExtractor func(*http.Request) string, logWriter io.Writer) *echo.Echo {
e := echo.New()
e.HideBanner = true

Expand All @@ -137,9 +133,16 @@ func setupEcho(ipExtractor func(*http.Request) string) *echo.Echo {
e.IPExtractor = ipExtractor

e.Use(middleware.RequestID())
e.Use(middlewares.VerboseBodyLogger(logWriter, config.EnVar.VerboseHTTPBodyLogging, config.EnVar.VerboseHTTPBodyLogLimit))
e.Use(middleware.Recover())
e.Use(middlewares.Logger())
e.Use(middleware.BodyLimit("1M"))
e.Use(middlewares.Logger(logWriter))
e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{
// Skip the voice route — it enforces its own 10 MB limit in the handler.
Skipper: func(c echo.Context) bool {
return c.Request().URL.Path == "/api/transactions/voice"
},
Limit: "1M",
}))
e.Use(middleware.RateLimiter(
middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Expand Down
12 changes: 12 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ services:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:8.4-alpine
container_name: numex_redis
Expand All @@ -20,6 +26,12 @@ services:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

volumes:
postgres_data:
Expand Down
24 changes: 18 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module numex-api

go 1.25.9
go 1.25.10

require (
github.com/alexedwards/argon2id v1.0.0
Expand All @@ -11,11 +11,12 @@ require (
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo/v4 v4.15.1
github.com/minio/minio-go/v7 v7.0.100
github.com/polarsource/polar-go v0.12.0
github.com/redis/go-redis/v9 v9.18.0
github.com/stretchr/testify v1.11.1
github.com/svix/svix-webhooks v1.89.0
golang.org/x/crypto v0.47.0
golang.org/x/crypto v0.51.0
golang.org/x/time v0.14.0
google.golang.org/api v0.197.0
google.golang.org/genai v1.51.0
Expand All @@ -29,8 +30,10 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand All @@ -43,12 +46,20 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/spyzhov/ajson v0.8.0 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect
Expand All @@ -58,11 +69,12 @@ require (
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/net v0.49.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
Expand Down
Loading
Loading