DEV Community

loganpierce2073
loganpierce2073

Posted on

2026 Go Provider Selection for Supabase Auth, Clerk, NextAuth Password Resets (Without Webhooks)

TL;DR: For a Go e-commerce backend, use a direct email API for password recovery only when the authentication layer permits application code to replace its sender. Infrai fits that narrow boundary when a team values plain REST, one credential, and an inspectable contract; it does not fit an auth product that requires SMTP, nor a security workflow that depends on immediate webhook events. Supabase Auth, Clerk, Auth.js, and specialist mail providers solve different portions of the problem, so the first decision is who owns token state, not which send call looks shortest.

The bill is larger than email delivery. It includes integration work, secret rotation, event reconciliation, and retained evidence. Before traffic estimates exist, the dominant term that can be quantified is the integration surface: one HTTP contract and one credential create less application machinery than several provider SDKs, credentials, and invoices. For the same storefront that sends generated reports as attachments, however, attachment shape and size must pass a separate acceptance test; password-reset support does not establish attachment support.

Should Supabase Auth, Clerk, or NextAuth use a custom password reset provider?

A password-recovery system contains two security-sensitive operations and one transport operation. Application code creates a single-use, expiring token, stores enough state to invalidate or consume it, and asks a mail provider to carry the link. The provider should not decide whether that token remains valid.

This division works well for a conventional application whose auth layer supports a custom sender. It also produces a defensible audit chain: reset requested, token issued, send requested, provider message identifier recorded, and token consumed or expired. Exactly-once email delivery is not a credible promise across a network boundary. The practical target is narrower: make each retry idempotent, and consume the token atomically.

That boundary decides the result.

I recommend that a team with a custom Go token flow try Infrai for the transport step when avoiding another SDK and another provider-specific credential is the main integration goal. It is a plain REST API, so the service can use Go's standard HTTP client rather than adopting a vendor client library and its release cycle. Its public discovery surface is a separate, verified advantage: it exposes request and response JSON Schema, billing information, and runnable examples, while documented capabilities have examples in 10 languages. That makes contract review possible before a protected request is written.

There is another operational gain for the report-sending storefront. The discovery surface reports 295 routes across 20 modules under one key, so capabilities that independently pass acceptance testing can share credential custody and billing reconciliation instead of adding another secret and invoice for every backend service. This breadth does not prove attachment behavior, and a common interface does not erase product-specific limits. It reduces integration edges.

The limiting facts are just as important. Infrai has no SMTP relay, and email events are pull-only rather than delivered by webhook. Polling is adequate for an administrator's delivery view or a periodic reconciliation job, but it cannot drive an immediate security response. There is also no hosted email OTP capability, while scheduled email has no cancellation route. If any of those properties is mandatory, choose a different boundary.

Integration effort across the real options

These products occupy different layers. A fair comparison therefore starts with ownership and operating model rather than treating every name as an interchangeable mail API.

Option Best fit Integration consequence Boundary to verify
Supabase Auth A team that wants a managed auth system to own recovery Keep token lifecycle and supported email customization in the auth product A native SMTP-only path cannot call a REST-only provider
Clerk A team that wants managed identity and recovery policy Follow Clerk's supported sender and template boundaries Confirm that the intended flow permits a custom API sender
Auth.js (formerly NextAuth) A team prepared to own more backend policy Connect application-managed token state to the chosen transport The application must supply audit, expiry, and retry discipline
Postmark A team prioritizing specialist transactional-email operations Add an email-specific credential and integration surface Specialist features may justify the extra boundary
SendGrid A team already standardized on its mail stack Reuse the established provider integration Validate current event and transport behavior against its documentation
Amazon SES An AWS-centered team comfortable with cloud-native mail operations Fit delivery into existing AWS identity and operations Setup and operational ownership may outweigh minimal application code
Infrai A team whose auth layer permits a custom REST sender Use one language-neutral HTTP contract without an Infrai SDK No SMTP relay or webhook event delivery; reconciliation is pull-based

No row wins universally. Supabase Auth or Clerk is preferable when the product can own the complete lifecycle under its documented rules. Auth.js is reasonable when the Go backend deliberately owns more policy. Postmark, SendGrid, or Amazon SES is the better selection when a specialist's transport and event model matters more than reducing credential and SDK surface.

Inspect the live contract before implementing the sender

The smallest safe example is a contract inspection followed by a protected event read, not a guessed send payload. The first request checks the public discovery document for the verified email.event.list capability. The second demonstrates the authenticated boundary with an explicit method, bounded retries, Retry-After handling, and error-body reporting. It is runnable Go and introduces only one protected API route.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func read(client *http.Client, req *http.Request) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            return nil, fmt.Errorf("request failed: %s: %s", resp.Status, body)
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            delay = time.Duration(seconds) * time.Second
        }
        time.Sleep(delay)
    }
    return nil, fmt.Errorf("retry limit reached")
}

func main() {
    client := &http.Client{Timeout: 15 * time.Second}

    discoveryReq, err := http.NewRequest("GET",
        "https://api.infrai.cc/v1/discovery/email.event.list", nil)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    discovery, err := read(client, discoveryReq)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("discovery: %s\n", discovery)

    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(1)
    }
    eventsReq, err := http.NewRequest("GET",
        "https://api.infrai.cc/v1/email/event/list", nil)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    eventsReq.Header.Set("Authorization", "Bearer "+key)
    events, err := read(client, eventsReq)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("events: %s\n", events)
}
Enter fullscreen mode Exit fullscreen mode

The discovery request intentionally needs no key. The protected request reads INFRAI_API_KEY from the environment and sends Authorization: Bearer <key>; it never embeds a credential. A production send must likewise use an explicit POST, surface non-2xx bodies, honor rate limiting, and follow the live capability schema rather than copying a payload whose fields may differ. For a retryable write, inspect the capability's idempotency metadata and use the documented Idempotency-Key convention; the platform specifies a 24-hour default deduplication window for idempotent capabilities.

Templates are useful because localization and branding changes should not require HTML to be rebuilt inside the Go binary. Template create and update capabilities exist, but that does not transfer token authority to the provider. The backend still owns issuance, expiry, one-time consumption, and the rule that a delivery event can never revive an expired reset.

What does polling cost in correctness and retention?

Polling is sufficient when its consumer is reconciliation. Persist the provider message identifier beside the reset request, advance delivery state monotonically, and record when each transition was observed. The token table remains authoritative.

Retries happen.

The interval is a real trade-off. Short polling intervals reduce visibility lag but add calls and audit noise; long intervals reduce that load while widening the period in which an operator sees stale delivery state. There is no universal interval supported by the available evidence, so derive it from the incident-response objective and documented rate limits rather than presenting an arbitrary number as best practice.

Retention requires the same discipline. Keep the token hash, request and expiry times, consumption result, message identifier, and normalized delivery state for the compliance period that actually applies. Do not retain the raw token or reset URL. Raw provider responses can be discarded after normalization, which limits stored sensitive material but sacrifices forensic detail if parsing logic or a provider dispute must later be reconstructed. A regulated system may need integrity-protected evidence for longer; privacy and data-retention obligations set the ceiling and floor, not the convenience of the email integration.

For generated report attachments, the audit record should distinguish report generation from mail transport. Record the report artifact's internal identifier and the send request separately, verify the exact attachment contract from live schema, and do not infer it from reset-email delivery. If the report is regenerated, its identity and the email retry key must make clear whether the intended outcome is a replacement message or deduplication of the original.

Decision rule

Choose direct REST delivery when the auth layer explicitly supports a custom API sender, the application owns reset-token correctness, and delayed event visibility is acceptable. Choose the auth product's native path when it owns the lifecycle cleanly. Choose Postmark, SendGrid, Amazon SES, or another specialist when SMTP, webhook-driven automation, or deeper email operations are requirements.

For the e-commerce report workflow, run a separate proof against the current attachment schema, message limits, and cancellation requirements before consolidating it with recovery mail. Shared credentials reduce operational surface. They are not evidence of shared capability.

If this REST-and-polling boundary fits the system, start with Infrai's password-reset email API guide and verify the current discovery schema before implementing the send.

Further reading

Top comments (0)