Skip to main content
Last verified: 2026-02-12 | Commit scope: fa4aa75

Overview

The payment service is an isolated microservice with its own database, VPC, and deployment lifecycle. Responsibilities:
  • Payment initiation and processing
  • Price quotes and pricing configuration
  • Webhook handling from payment providers
  • Event publishing for payment lifecycle
Isolation boundaries:
  • Separate Go module (Go Workspace in hoodcloud/payment-service)
  • Separate PostgreSQL instance
  • Network isolation (designed for separate VPC)
  • No card data touches our systems (SAQ A compliance)

Communication Architecture

Sync: gRPC + mTLS

gRPC MethodDescription
InitiatePaymentCreates payment session
GetQuoteReturns pricing for items
GetPaymentRetrieves payment state
CancelPaymentCancels pending payment
Security: TLS 1.3 minimum, client certificate verification (mTLS), CN auth interceptor validates client certificate CN against allowed list (payment-service/internal/server/grpc.go:cnAuthInterceptor). Standard gRPC health check service (grpc.health.v1.Health).

Async: NATS JetStream

EventSubjectConsumer Action
PaymentCompletedEventpayment.completedCreate subscriptions, trigger provisioning
PaymentFailedEventpayment.failedLog failure, notify user
PaymentCanceledEventpayment.canceledUpdate records
Delivery: Durable consumer (main-app-payment), manual ACK, idempotency via Redis store.
See also: Workflows — NATS Consumers for the consumer implementation in the main app.

Database Schema

Separate PostgreSQL instance with four tables:
TablePurpose
paymentsCore payment record (status, amount, provider reference, crypto details)
payment_line_itemsLine items per payment (chain_profile_id, node_type, duration, pricing)
payment_eventsAudit trail (actor, event_type, request_id, IP, JSONB details)
customersMinimal user mirror (id matches main app user_id, wallet_address, email)

Payment Status Flow

Data Isolation

DataPayment ServiceMain App
User identityMirror (customer_id only)Primary
Payment recordsPrimaryReference only
SubscriptionsReference onlyPrimary

Pricing Service

Config-based (not database-driven) for simplicity and auditability. File: payment-service/config/pricing.yaml
pricing:
  celestia-mocha:
    light:
      1w: 999       # $9.99
      1m: 2999      # $29.99
    full:
      1w: 1999
      1m: 5999

crypto:
  supported_currencies: [USDC, USDT, ETH]
  exchange_rates:
    USDC: "1.0"
    USDT: "1.0"
    ETH: "2500.0"
  quote_validity: 15m

session:
  expiry: 30m
Lookup: PricingService.GetPrice(chainProfileID, nodeType, duration) in payment-service/internal/service/pricing.go.

Multi-Provider Architecture

Multiple providers active simultaneously, keyed by payment method. Provider selection based on method field in payment request. Registered in map[PaymentMethod]Provider during startup.

Provider Adapter Interface

File: payment-service/internal/adapters/provider.go
type Provider interface {
    Name() string
    InitiatePayment(ctx context.Context, req InitiateRequest) (*InitiateResult, error)
    GetPaymentStatus(ctx context.Context, providerRef string) (*StatusResult, error)
    CancelPayment(ctx context.Context, providerRef string) error
    HandleWebhook(ctx context.Context, payload []byte, signature string) (*WebhookResult, error)
}

Available Adapters

AdapterMethodStatusNotes
stripecardImplementedStripe Checkout Sessions, webhook signature verification
tempocryptoImplementedTIP-20 TransferWithMemo, payment ID as bytes32 memo
mockanyImplementedConfigurable delay and failure rate for testing

Stripe Adapter

Package: payment-service/internal/adapters/stripe/ Flow: InitiatePayment -> Stripe Checkout Session -> user completes payment -> Stripe webhook (POST /webhooks/stripe) -> HandleWebhook verifies Stripe-Signature -> CompletePayment -> NATS payment.completed. Webhook events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired. Config: STRIPE_ENABLED=true. Credentials (stripe_secret_key, stripe_webhook_secret) stored in Vault.

Tempo Adapter

Package: payment-service/internal/adapters/tempo/ Flow: InitiatePayment returns receiver address + memo (payment ID as hex) -> user calls transferWithMemo() on TIP-20 contract -> background watcher detects TransferWithMemo event -> CompletePayment -> NATS payment.completed. Config: TEMPO_ENABLED=true, TEMPO_RECEIVER_ADDRESS.

Payment Methods Endpoint

GET /api/v1/payment-methods returns active methods based on enabled providers:
{"methods": ["card", "crypto"]}

Main App Integration

Payment Initiation

File: internal/api/handler_payment.go POST /api/v1/payments initiates a checkout session via gRPC to the payment service.
FieldRequiredDescription
itemsYesLine items (chain_profile_id, node_type, duration)
subscription_idsNopending_payment subscriptions to link
methodYes"card" or "crypto"
crypto_currencyWhen method=crypto"USDC", "USDT", or "ETH"
idempotency_keyYesClient-generated key for safe retries
return_url / cancel_urlYesRedirect URLs after checkout
Subscription linking: Handler validates each subscription ID (exists, owned by caller, status pending_payment), initiates payment via gRPC, sets payment_id on each subscription via UpdatePaymentID.

gRPC Client

File: internal/grpc/payment_client.go Connects via mTLS. Credentials from Vault or file paths.

gRPC Service Definition

File: payment-service/proto/payment.proto
service PaymentService {
  rpc InitiatePayment(InitiatePaymentRequest) returns (InitiatePaymentResponse);
  rpc GetPayment(GetPaymentRequest) returns (GetPaymentResponse);
  rpc GetQuote(GetQuoteRequest) returns (GetQuoteResponse);
  rpc CancelPayment(CancelPaymentRequest) returns (CancelPaymentResponse);
}

Vault Integration

Package: payment-service/internal/vault/ Independent Vault client (separate from main app). AppRole authentication with token renewal. Retrieves PaymentCredentials (DB password, Redis password, Stripe keys, NATS CTRL account signing seed).
See also: Vault for Vault setup, secret structure, and operations. See Environment Variables for the complete payment service Vault configuration variables.

Development

cd payment-service
make proto       # Generate protobuf code
make migrate-up  # Run database migrations
make build       # Build binary
make test        # Run tests
Docker Compose: infrastructure/docker/docker-compose.payment.yml with dedicated Caddy reverse proxy for TLS termination.
ServicePortDescription
caddy-payment443HTTPS (TLS termination, Let’s Encrypt)
postgres-payment5433Payment database
payment-service50051gRPC API
payment-service8085HTTP health checks
payment-service9086Prometheus metrics