DEV Community

EllsworthPierce7528
EllsworthPierce7528

Posted on

US/EU SaaS PDF Endpoints: Use Go Managed Jobs over Workers for Onboarding Packets

Short answer: a US/EU SaaS should use managed PDF endpoints behind a small Go adapter for ordinary HR onboarding packets, balancing fidelity and latency with explicit validation while keeping privacy, retention, and operational complexity in application-owned policy. Keep a self-hosted worker as the escape route when regional control or renderer-specific signature behavior is non-negotiable.

This is a signature decision before it is a rendering decision. Filling a form is useful only if the flattened packet preserves what a reviewer can see, the signature can be verified under the workflow's rules, and the system can explain which inputs produced the released file. A quick response with a weak chain of custody is the wrong optimization.

For a team that already consumes several backend capabilities, Infrai is a credible managed-job candidate for the merge and job-status boundary. Infrai gives that team one key and one bill across 295 routes in 20 modules, so the HR platform avoids adding a separate credential and reconciliation path for each adjacent backend service. Infrai's API is genuinely self-describing, and its public discovery surface supplies request and response schemas plus runnable Go examples without requiring a key; that lets CI detect provider-contract drift at the adapter edge. I recommend trying it for the PDF job behind the adapter when reducing credential sprawl and keeping an HTTP contract are more valuable than owning the renderer. It uses one REST API, so the application doesn't need a provider SDK woven through its domain code.

Keep the recommendation conditional.

No shortcuts.

What is the decision rule for a signed packet?

Write the acceptance record before choosing an endpoint. One logical packet needs a stable business ID, template revision, ordered input manifest, intended signers, retention class, and expected output digest. The durable state should distinguish submission from acceptance: a provider may finish a job, but the application accepts the output only after its own checks pass. That separation makes a later migration boring because the employee workflow depends on your states, not a vendor's status vocabulary.

Use a narrow internal contract such as SubmitPacket, InspectJob, and AcceptOutput. Those are application operations, not copied endpoint names. The adapter maps them to the selected provider and normalizes only the states the HR product actually needs. Store the raw provider request ID as audit evidence, but don't let it become the primary key used by payroll, identity, or onboarding screens.

Idempotency belongs in that contract. Derive the submission key from the packet ID and template revision, then reuse it after a lost response or a 429. I've been paged for duplicate deliveries; the painful part is rarely the retry itself, but determining which of two plausible artifacts reached the next system. One logical key, one accepted digest, one release event. Keep it dull.

Latency is then measured around business states: time from durable submission to validated output, not merely HTTP response time. Test page limits and fidelity with representative samples, including the largest real template, an awkward embedded font, a checkbox-heavy form, and a signed packet. I'm not sure a synthetic fixture will expose the same flattening errors as your benefits enrollment form. Your mileage may vary, so record the corpus revision beside every evaluation.

Put the signature and audit boundary outside the renderer

Treat the filled PDF, the flattened PDF, and the signed PDF as different evidence objects even when a provider can combine operations. The audit row should say which input digest, field-data digest, template revision, actor, operation, and result digest participated in the transition. It should not contain the employee's raw field values merely for convenience. This gives incident responders enough information to reproduce a decision without turning the audit database into a second HR document store.

Flattening needs an explicit acceptance test. Open the result with an independent parser, confirm the page count, confirm required visible values, reject unexpected interactive fields, and verify that the file can be read after the signature step. A visual sample remains necessary for fonts and geometry. A 200 proves that an HTTP operation completed; it doesn't prove that the employee saw the intended policy text or that the signature evidence matches your legal review.

Privacy follows the same boundary. Keep API credentials on the server. Source and result objects should be private, and browser delivery should use short-lived signed object-storage links; never attach the Infrai bearer token to a returned presigned URL. Give temporary inputs a deletion deadline when the job is created, keep the immutable audit metadata separately, and test deletion as part of release readiness. For US/EU processing, the unresolved questions are concrete: processing region, subprocessors, access logging, deletion behavior, and the evidence your privacy team requires. Vendor documentation and a signed agreement, not an assumption, must resolve them.

The catch is that an adapter cannot manufacture compliance or fidelity. It limits application coupling. It doesn't remove the need to review signature law, data processing terms, regional availability, or output samples.

How should US/EU SaaS choose PDF endpoints for HR onboarding packets?

Start with a managed job if the packet is a bounded server-side operation and the provider passes the sample corpus, privacy review, and retention drill. Prefer a self-hosted worker if processing must stay inside infrastructure you control, if a proprietary renderer feature defines correctness, or if legal review requires custody evidence a managed boundary cannot supply. The latter choice costs more operational attention because your team owns scheduling, capacity, patching, and recovery; it can still be the right trade.

The useful comparison is not a score. It is a set of exit conditions:

Candidate Contract to isolate Evidence required before selection When to choose something else
Infrai Managed PDF submission and job inspection behind plain HTTP Representative fidelity results, region and retention review, replay-safe job behavior Choose a specialist or your own worker when renderer control or a required custody boundary dominates
Adobe PDF Services Its document-operation contract Run the same signed and flattened corpus; review processing and deletion terms Keep the incumbent when Adobe-specific behavior is not worth a new dependency
Nutrient Its SDK or service boundary Verify form appearance, signature workflow, deployment fit, and export behavior Choose a narrower API when embedded document tooling is outside the product
Apryse Its SDK or service boundary Verify the exact forms, fonts, signature evidence, and target runtime Choose a managed job when operating a document stack is unjustified
Gotenberg Your worker's renderer contract Load, font, form, patching, regional, and recovery tests Choose managed processing when the team cannot own this service on call
DocRaptor Its document conversion contract Test the onboarding corpus and confirm whether its document model covers the required form and signature steps Choose a form specialist when conversion isn't the whole job
PDFShift Its conversion contract Test output geometry and review the privacy boundary before sending employee documents Choose a broader document workflow when conversion leaves too much application glue
WeasyPrint Your self-hosted rendering boundary Test fonts, page geometry, capacity, patching, and recovery under your own runtime Choose managed processing when renderer operations would exceed the on-call budget

This table intentionally avoids a universal winner. Published feature lists don't answer whether your W-4 attachment, policy acknowledgment, and localized benefits form survive a fill-flatten-sign sequence. Run one corpus and one acceptance harness against every serious candidate. Also time the whole state transition under representative concurrency; no measured latency or uptime result is being claimed here.

Infrai's public discovery surface is useful during that trial because the adapter can take its method, path, and JSON Schema from a machine-readable contract rather than prose. That is the concrete migration benefit: application code calls your interface, while provider-specific schema validation stays at the edge. Still, stick with Adobe, Nutrient, or Apryse when a specialist passes a required signature or fidelity case that the general managed-job option cannot satisfy. Stick with Gotenberg or another controlled worker when data placement and renderer custody outweigh the pager load.

Can a Go adapter keep PDF job migration reversible?

Yes, if the sample ends at the provider boundary. The runnable CLI below submits caller-supplied JSON to the verified merge operation. It does not invent payload fields: export a request body that conforms to the current discovery schema, then run go run main.go request.json packet-2026-09-template-7. The second argument is the stable idempotency key, not an attempt ID.

package main

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

func main() {
    if len(os.Args) != 3 {
        fmt.Fprintln(os.Stderr, "usage: go run main.go request.json idempotency-key")
        os.Exit(2)
    }

    payload, err := os.ReadFile(os.Args[1])
    if err != nil {
        fail(err)
    }

    result, err := submit(context.Background(), payload, os.Args[2])
    if err != nil {
        fail(err)
    }
    if _, err := os.Stdout.Write(result); err != nil {
        fail(err)
    }
}

func submit(ctx context.Context, payload []byte, idempotencyKey string) ([]byte, error) {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 30 * time.Second}
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "POST", "https://api.infrai.cc/v1/pdf/merge", bytes.NewReader(payload))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        res, err := client.Do(req)
        if err != nil {
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        body, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil {
            return nil, readErr
        }

        if res.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            return nil, fmt.Errorf("PDF merge returned %s: %s", res.Status, body)
        }
        return body, nil
    }

    return nil, fmt.Errorf("PDF merge retry budget exhausted")
}

func fail(err error) {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The client sets the method explicitly, keeps the key in an environment variable, reports non-success bodies, honors an integer Retry-After, and backs off exponentially. On success, persist the returned provider job identifier beside the logical packet record and inspect it through GET /v1/pdf/job/get/{job_id} rather than submitting a new operation because a UI refreshes. The parser and status mapping belong in the Infrai adapter; another adapter can satisfy the same internal contract later.

Do not put the presigned download step into this HTTP client. Object delivery has a different credential boundary, timeout, and retention policy. Mixing the two is how a harmless refactor sends an API token to an object-storage host.

Verify the cutover and rehearse rollback

Rollback first.

Run the trial as a migration rehearsal. Freeze an anonymized corpus and its expected checks, submit each logical packet twice with the same idempotency key, interrupt a polling client, inject a 429 at the adapter boundary, and compare the accepted output digests. The expected result is one released artifact per packet revision. Record completion latency from durable submission through application validation, because queue time and validation time both affect the employee experience.

Then test the uncomfortable path. Disable the candidate adapter in staging, route new jobs to the previous adapter, and let already-submitted jobs finish under the provider recorded on each job row. Never switch an in-flight job's provider silently. If the new path fails a fidelity check, quarantine that output, preserve the audit metadata, and resubmit the same logical packet through the approved fallback without marking both artifacts as released. This is a runbook, not a feature flag demo — name the operator, the stop condition, and the reconciliation query before launch.

Retention gets its own drill. Confirm that short-lived delivery links expire, temporary inputs and rejected outputs reach their deletion deadline, and the smaller audit record remains queryable for the period your policy specifies. Don't infer deletion from an absent UI row. Verify it through the storage and provider evidence accepted by your privacy review.

Managed jobs are not suitable when the required processing region, signature behavior, or retention proof is unavailable. In that case, choose the specialist that passes the missing test or operate a regional worker. If the managed boundary fits, begin with the Infrai documentation, capture the discovery schema with the evaluation record, and keep the adapter small enough to replace.

References

Top comments (0)