Go replacement for DysonNetwork.Ring — the Solar Network's realtime service: notifications, push delivery (FCM / APNs / UnifiedPush / SOP), email sending plans and delivery observability. A strict 1:1 behavioral port, following the house pattern established by the Stargate port of Padlock/Passport.
cmd/metoer/ entrypoint (HTTP :8080 + gRPC :9090, TLS optional)
internal/config TOML config + METOER_* env overrides
internal/db, migrate GORM PostgreSQL handle + embedded SQL migrations
internal/store GORM-backed Postgres access layer (mirrors EF Core
queries incl. the global soft-delete filter)
internal/model entity + wire models (snake_case JSON, nulls included)
internal/queue in-process async dispatcher (buffered channel + workers)
internal/push PushService port: senders, SOP streams, replay,
websocket fan-out, invalid-token flush buffer
internal/email SMTP sender + email sending-plan state machine
internal/observability delivery records (never break the delivery path)
internal/grpcclient outbound clients (stargate: account/auth/permission/
profile/action-log; blade: websocket)
internal/grpcserver DyRingService + capabilities + reflection + health
internal/httpserver gin + /api controllers + swagger manifest
internal/middleware DysonTokenAuthHandler + RemotePermissionMiddleware ports
internal/scheduler the four Quartz jobs (UTC schedules)
internal/events websocket_push publisher + filesystem listener
Requires Postgres (dyson_ring), Redis and NATS (JetStream for
account_events / accounts.last_active, core NATS for websocket_push).
make run # CONFIG_PATH=config.example.tomlCONFIG_PATH selects the TOML file; METOER_* env vars override individual
keys (METOER_DATABASE__DSN, METOER_SERVICES_STARGATE__GRPC,
METOER_NATS_TARGET, …). Push keys (Keys/Solian.json, Keys/Solian.p8)
are mounted at deploy time, never baked into the image.
- API JSON: snake_case with nulls INCLUDED (Ring's
AddJsonOptionssets noDefaultIgnoreCondition) — unlike Stargate's omit-nulls convention, do not addomitemptyto API-facing fields. Enum values are ints; instants are RFC3339 UTC seconds.metadictionary keys are snake-cased on outbound responses (STJDictionaryKeyPolicy). - Async dispatch (push/email): in-process —
internal/queuequeues jobs on a buffered channel drained by a worker pool (size =ConsumerCount). Delivery is exactly-once-per-enqueue: no redelivery, no consumer state, no replay-on-restart; jobs buffered at shutdown are lost. Per-device push failures are terminal (logged, withfailure/invalid_tokendelivery records); invalid tokens are deleted by the 5-minute flush job. - NATS (events, not the push queue): core subject
websocket_pushcarries the{namespace,target,ids,excluded_device_ids,packet}envelope (packet = base64 protojsonDyWebSocketPacketwithnotifications.new). JetStreamaccount_events/accounts.last_active(PascalCaseLastActiveEvent) is published by the auth middleware (throttled 1m per account); the filesystem listener consumesfilesystem.file.updated.v1. - Redis keys (shared with the C# fleet via the
dyson:prefix):ring:sop:replay:*(+ lock),auth:session:*,auth:profile:*,auth:last_seen_touch:*. - Auth runs over gRPC to Stargate (
DyAuthService.Authenticate); permissions overDyPermissionService(Stargate), with the OAuth scope gate and superuser bypass. With noservices.stargatetarget the middleware leaves requests anonymous (auth-gated routes 401; the C#RpcExceptionpath degrades the same way with the dependency down). - Filesystem listener:
filesystem.file.updated.v1onfilesystem_eventsis consumed and acked; Ring's schema has no reference-typed jsonb columns, so the updater is a no-op (0 rows). - Migration DDL is idempotent (
CREATE ... IF NOT EXISTS, no DROPs) — thedyson_ringdatabase stays shared with the C# fleet until cutover.
- Blade config delta:
[services] metoer = { http = "http://localhost:8080", grpc = "localhost:9090" }; addmetoerto[endpoints] serviceNames. - Caddy delta so old clients keep calling
/ring/**:@ring path /ring/*→rewrite @ring /metoer{path}before the finalreverse_proxy(pattern from Stargate's Caddyfile). - Point the fleet's
services__ring__grpc__0env at Metoer's gRPC endpoint, then stop the C# Ring instances (same DB, NATS and Redis).
- Swagger UI/manifest is hand-built (byte-identity with Swashbuckle is a dev tool concern only).
metasnake-casing handles nestedmap[string]anykeys (STJ's policy applies toDictionaryinstances; passthrough of rawJsonElementsubtrees is not replicated).- The auth middleware uses the Go shared-cache format for
auth:profile:*(plain value vs the C# HSET envelope); both sides treat the other's format as a miss and refetch. - The in-process SOP stream queue is unbounded (slice-backed cond queue),
matching
Channel.CreateUnboundedsemantics.