Design the backend behind instant freight quotes: parallel carrier rate calls with per-carrier API keys, quotes that expire, a booking step that cannot double book or book a stale price, confirmation events, and bills of lading stored in your own bucket, built by hand on AWS or in TypeScript with Encore.

A shipper who asks for a freight quote expects prices from several carriers in a few seconds, and expects the price they click to be the price they get. Behind that sits a backend that calls carrier rate APIs in parallel with separate credentials, stores every quote with an expiry, books exactly once even when the shipper double clicks or the carrier times out, and fetches the bill of lading the carrier issues for the shipment.
This guide designs that backend for less-than-truckload and full-truckload shipments, first as a plain AWS layout and then in TypeScript with Encore. Pallet, a supply chain technology company that spent three years building a transportation management system, runs an event-driven AI logistics platform on Google Cloud with Encore.
A shipper's quote request has to survive a slow carrier, a double click and a timeout, and the booking that follows has to be the same booking if it is retried. The design below is the smallest set of tables and statements that does that on RDS, Secrets Manager, SNS, SQS and S3.
Rates and credentials. A quote request fans out to every carrier's rate API at once, each with its own timeout, so one slow carrier costs the shipper that carrier's prices and nothing else. Promise.allSettled with AbortSignal.timeout does this in Node.js. Each carrier issues its own API key, stored in Secrets Manager and allowed by the task role's policy.
Tables. One quotes row holds a rate through its whole life. expires_at is the earlier of the carrier's validity and your own cap, such as 30 minutes. booking_id is unique, so a retry cannot attach a second carrier booking:
CREATE TABLE quotes (
id TEXT PRIMARY KEY,
shipper_uid TEXT NOT NULL,
carrier TEXT NOT NULL,
carrier_quote_ref TEXT NOT NULL,
price_cents BIGINT NOT NULL,
currency TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'quoted'
CHECK (status IN ('quoted', 'booking', 'booked')),
booking_id TEXT UNIQUE,
booking_number TEXT,
bol_key TEXT
);
UPDATE ... RETURNING moves the quote from quoted to booking only while it is unexpired. A second tab or a double click finds no row and stops. The booking_id assigned here is sent to the carrier as the Idempotency-Key, the pattern the IETF Idempotency-Key header draft standardizes, so a retry after a timeout returns the original carrier booking:UPDATE quotes
SET status = 'booking', booking_id = COALESCE(booking_id, $1)
WHERE id = $2 AND shipper_uid = $3 AND status = 'quoted' AND expires_at > now()
RETURNING carrier, carrier_quote_ref, booking_id;
If the carrier rejects the booking, a second UPDATE returns the row to quoted so the shipper can pick another rate. If the process crashes between the claim and the carrier's answer, a short reconciliation job selects rows stuck in booking past a few minutes with FOR UPDATE SKIP LOCKED, asks the carrier about the stored booking_id, and marks them booked or quoted.
Confirmation events. After the carrier accepts, the API marks the quote booked and publishes a booking-confirmed message to SNS. An SQS queue with a redrive policy is the subscription, so a consumer that fails while talking to the carrier retries, then lands on a dead-letter queue.
Documents. The consumer downloads the bill of lading into a private S3 bucket, keyed by booking_id, and writes bol_key only after the object exists, so a redelivered message can skip the download with HeadObject. In the US, a for-hire motor carrier has to issue a receipt or bill of lading listing the consignor, consignee, origin, destination, packages, freight description and weight under 49 CFR 373.101, and shippers expect to find it next to the booking, served through a short-lived signed GET URL.
Encore brings infrastructure from code to TypeScript and Go backends, provisioning what a service declares in its code in your own AWS or GCP account. Here one freight service declares carrier secrets, a Postgres database, a topic and a bucket, and on AWS the deploy creates RDS, SNS and SQS with a dead-letter queue, and S3, with secret values in Secrets Manager, as the AWS infrastructure docs describe.

Run in your terminal to get started locally.
The freight directory is a service, declared in its encore.service.ts:
// freight/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("freight");
The database lives in its own file so the quote and document code can share it:
// freight/db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";
export const db = new SQLDatabase("freight", { migrations: "./migrations" });
The migration creates the quotes table used above, including service_level and transit_days on each rate:
CREATE TABLE quotes (
id TEXT PRIMARY KEY,
shipper_uid TEXT NOT NULL,
carrier TEXT NOT NULL,
carrier_quote_ref TEXT NOT NULL,
service_level TEXT NOT NULL,
price_cents BIGINT NOT NULL,
currency TEXT NOT NULL,
transit_days INT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'quoted'
CHECK (status IN ('quoted', 'booking', 'booked')),
booking_id TEXT UNIQUE,
booking_number TEXT,
bol_key TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
The carrier client reads one secret per carrier and calls every rate API in parallel. The carrier names and URLs are placeholders for whichever carriers or rate aggregators you contract with:
// freight/carriers.ts
import { secret } from "encore.dev/config";
import log from "encore.dev/log";
// One API key per carrier, set per environment with `encore secret set`.
const carrierKeys = {
northline: secret("NorthlineApiKey"),
baltic: secret("BalticFreightApiKey"),
};
export type CarrierId = keyof typeof carrierKeys;
const baseUrls: Record<CarrierId, string> = {
northline: "https://api.northline.example/v2",
baltic: "https://api.balticfreight.example/v1",
};
export interface Shipment {
originZip: string;
destZip: string;
weightKg: number;
pallets: number;
pickupDate: string; // YYYY-MM-DD
}
export interface CarrierRate {
carrier: CarrierId;
serviceLevel: string;
priceCents: number;
currency: string;
transitDays: number;
carrierQuoteRef: string;
validUntil: Date;
}
async function call<T>(carrier: CarrierId, path: string, body: unknown, idempotencyKey?: string): Promise<T> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${carrierKeys[carrier]()}`,
};
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const res = await fetch(baseUrls[carrier] + path, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: AbortSignal.timeout(8000),
});
if (!res.ok) throw new Error(`${carrier} ${path} returned ${res.status}`);
return (await res.json()) as T;
}
interface RatesResponse {
rates: { service: string; amount_cents: number; currency: string; transit_days: number; quote_id: string; expires_at: string }[];
}
// Ask every carrier at once. A slow or failing carrier drops out of the
// result instead of failing the whole quote request.
export async function fetchRates(s: Shipment): Promise<CarrierRate[]> {
const carriers = Object.keys(carrierKeys) as CarrierId[];
const results = await Promise.allSettled(
carriers.map(async (carrier) => {
const r = await call<RatesResponse>(carrier, "/rates", s);
return r.rates.map((x) => ({
carrier,
serviceLevel: x.service,
priceCents: x.amount_cents,
currency: x.currency,
transitDays: x.transit_days,
carrierQuoteRef: x.quote_id,
validUntil: new Date(x.expires_at),
}));
}),
);
return results.flatMap((r, i) => {
if (r.status === "fulfilled") return r.value;
log.warn("carrier rate request failed", { carrier: carriers[i], error: String(r.reason) });
return [];
});
}
// The booking ID is sent as the idempotency key, so retrying after a
// timeout returns the same carrier booking instead of creating a second one.
export async function bookWithCarrier(carrier: CarrierId, carrierQuoteRef: string, bookingId: string) {
const r = await call<{ booking_number: string; bol_url: string }>(
carrier, "/bookings", { quote_id: carrierQuoteRef }, bookingId);
return { bookingNumber: r.booking_number, bolUrl: r.bol_url };
}
function allowedDocumentUrl(carrier: CarrierId, url: string): URL {
const parsed = new URL(url);
const allowed = new URL(baseUrls[carrier]);
const defaultPort = (u: URL) => u.port || (u.protocol === "https:" ? "443" : "80");
if (
parsed.protocol !== "https:" ||
parsed.hostname !== allowed.hostname ||
defaultPort(parsed) !== defaultPort(allowed)
) {
throw new Error(`${carrier} document URL is not on the carrier origin`);
}
return parsed;
}
export async function downloadDocument(carrier: CarrierId, url: string): Promise<Buffer> {
const parsed = allowedDocumentUrl(carrier, url);
const res = await fetch(parsed, {
headers: { Authorization: `Bearer ${carrierKeys[carrier]()}` },
signal: AbortSignal.timeout(15000),
redirect: "error",
});
if (!res.ok) throw new Error(`${carrier} document download returned ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
Each secret is set per environment with encore secret set, and the secrets docs cover how values are stored and loaded. Secret values are not stored in the repository, so a coding agent working on the carrier client sees NorthlineApiKey as a name and never the key itself. The quote and booking endpoints sit behind the app's auth handler:
// freight/quotes.ts
import { api, APIError } from "encore.dev/api";
import { randomUUID } from "node:crypto";
import { getAuthData } from "~encore/auth";
import { db } from "./db";
import { CarrierId, Shipment, bookWithCarrier, fetchRates } from "./carriers";
import { bookingConfirmed } from "./documents";
// Our own cap on how long a price can be booked, even if a carrier allows longer.
const MAX_QUOTE_MINUTES = 30;
interface Quote {
id: string;
carrier: CarrierId;
serviceLevel: string;
priceCents: number;
currency: string;
transitDays: number;
expiresAt: Date;
}
export const requestQuotes = api(
{ expose: true, auth: true, method: "POST", path: "/quotes" },
async (s: Shipment): Promise<{ quotes: Quote[] }> => {
const { userID } = getAuthData()!;
if (s.weightKg <= 0 || s.pallets <= 0) {
throw APIError.invalidArgument("weight and pallet count must be positive");
}
const rates = await fetchRates(s);
if (rates.length === 0) throw APIError.unavailable("no carrier returned a rate");
const cap = new Date(Date.now() + MAX_QUOTE_MINUTES * 60_000);
const quotes: Quote[] = [];
for (const r of rates) {
const q: Quote = {
id: randomUUID(),
carrier: r.carrier,
serviceLevel: r.serviceLevel,
priceCents: r.priceCents,
currency: r.currency,
transitDays: r.transitDays,
expiresAt: r.validUntil < cap ? r.validUntil : cap,
};
await db.exec`
INSERT INTO quotes (id, shipper_uid, carrier, carrier_quote_ref, service_level,
price_cents, currency, transit_days, expires_at)
VALUES (${q.id}, ${userID}, ${q.carrier}, ${r.carrierQuoteRef}, ${q.serviceLevel},
${q.priceCents}, ${q.currency}, ${q.transitDays}, ${q.expiresAt})`;
quotes.push(q);
}
quotes.sort((a, b) => a.priceCents - b.priceCents);
return { quotes };
},
);
interface Booking {
bookingId: string;
bookingNumber: string;
}
export const bookQuote = api(
{ expose: true, auth: true, method: "POST", path: "/quotes/:id/book" },
async ({ id }: { id: string }): Promise<Booking> => {
const { userID } = getAuthData()!;
// Only one request can move a quote from quoted to booking, and only
// before it expires, and only if the caller owns the quote.
const claimed = await db.queryRow<{ carrier: CarrierId; carrier_quote_ref: string; booking_id: string }>`
UPDATE quotes
SET status = 'booking', booking_id = COALESCE(booking_id, ${randomUUID()})
WHERE id = ${id} AND shipper_uid = ${userID} AND status = 'quoted' AND expires_at > now()
RETURNING carrier, carrier_quote_ref, booking_id`;
if (!claimed) {
const q = await db.queryRow<{ status: string; booking_id: string | null; booking_number: string | null }>`
SELECT status, booking_id, booking_number FROM quotes
WHERE id = ${id} AND shipper_uid = ${userID}`;
if (!q) throw APIError.notFound("quote not found");
if (q.status === "booked" && q.booking_id && q.booking_number) {
return { bookingId: q.booking_id, bookingNumber: q.booking_number }; // a retry after success
}
if (q.status === "quoted") throw APIError.failedPrecondition("quote expired, request new quotes");
throw APIError.aborted("booking already in progress");
}
let carrierBooking: { bookingNumber: string; bolUrl: string };
try {
carrierBooking = await bookWithCarrier(claimed.carrier, claimed.carrier_quote_ref, claimed.booking_id);
} catch (err) {
// Hand the quote back so the shipper can retry while it is still valid.
await db.exec`UPDATE quotes SET status = 'quoted' WHERE id = ${id} AND status = 'booking'`;
throw APIError.unavailable("carrier booking failed, try again", err as Error);
}
await db.exec`
UPDATE quotes SET status = 'booked', booking_number = ${carrierBooking.bookingNumber}
WHERE id = ${id}`;
await bookingConfirmed.publish({
bookingId: claimed.booking_id,
carrier: claimed.carrier,
bookingNumber: carrierBooking.bookingNumber,
bolUrl: carrierBooking.bolUrl,
});
return { bookingId: claimed.booking_id, bookingNumber: carrierBooking.bookingNumber };
},
);
bookQuote uses the claim UPDATE shown above: an expired price fails at booking time, a caller who does not own the quote gets a not-found error, a double click gets an aborted error, and a retry after success returns the same booking number.
The booking confirmed event drives the document step, and the subscription is safe to run twice:
// freight/documents.ts
import { api, APIError } from "encore.dev/api";
import { Subscription, Topic } from "encore.dev/pubsub";
import { Bucket } from "encore.dev/storage/objects";
import { getAuthData } from "~encore/auth";
import { db } from "./db";
import { CarrierId, downloadDocument } from "./carriers";
export interface BookingConfirmed {
bookingId: string;
carrier: CarrierId;
bookingNumber: string;
bolUrl: string;
}
export const bookingConfirmed = new Topic<BookingConfirmed>("booking-confirmed", {
deliveryGuarantee: "at-least-once",
});
// S3 on AWS, Cloud Storage on GCP, local disk under `encore run`.
export const billsOfLading = new Bucket("bills-of-lading", { versioned: false });
const bolKey = (bookingId: string) => `bol/${bookingId}.pdf`;
// Copy the bill of lading out of the carrier's system. The object key is the
// booking ID, so a redelivered event finds the file and stops.
const _ = new Subscription(bookingConfirmed, "store-bol", {
handler: async (e) => {
const key = bolKey(e.bookingId);
if (!(await billsOfLading.exists(key))) {
const pdf = await downloadDocument(e.carrier, e.bolUrl);
await billsOfLading.upload(key, pdf, { contentType: "application/pdf" });
}
await db.exec`UPDATE quotes SET bol_key = ${key} WHERE booking_id = ${e.bookingId}`;
},
// Carriers often generate the document a few minutes after booking.
retryPolicy: { minBackoff: "30s", maxBackoff: "30m", maxRetries: 20 },
});
export const billOfLadingLink = api(
{ expose: true, auth: true, method: "GET", path: "/bookings/:bookingId/bol" },
async ({ bookingId }: { bookingId: string }): Promise<{ url: string }> => {
const { userID } = getAuthData()!;
const row = await db.queryRow<{ bol_key: string | null }>`
SELECT bol_key FROM quotes
WHERE booking_id = ${bookingId} AND shipper_uid = ${userID}
`;
if (!row?.bol_key) throw APIError.notFound("bill of lading not stored yet");
const { url } = await billsOfLading.signedDownloadUrl(row.bol_key, { ttl: 900 });
return { url };
},
);
The retry policy gives the carrier time to produce the PDF, and per the Pub/Sub docs a message that exhausts its retries moves to a dead-letter queue. The object storage docs cover exists, upload and signed URLs, which become S3 or Cloud Storage on deploy. downloadDocument only fetches HTTPS URLs on that carrier's origin and refuses redirects, so the carrier API key is never sent to an arbitrary address. Shippers download the bill of lading through a 15 minute signed URL only when the booking belongs to them.
encore run starts Postgres, the topic and a local bucket, so the quote, book and store cycle can be exercised from the local development dashboard against a stub carrier before anything reaches AWS. A coding agent that adds a third carrier adds a secret and an entry in carrierKeys and baseUrls, sets the key with encore secret set, and tests the flow locally with no new queue or bucket to create.
Best fit: Freight and logistics teams building instant quotes and booking in TypeScript who want carrier credentials, booking events and documents in their own AWS or GCP account.
booking. A small reconciliation job that asks the carrier about bookings older than a few minutes, using the stored booking ID, settles them.booked before the event is published, so a crash in between skips the document step. An outbox table written in the same transaction closes that gap if carriers are slow to resend documents.No longer than the carrier's own quote validity, and often shorter. Store an expires_at per quote set to the earlier of the carrier's expiry and your own cap, such as 30 minutes, and check it in the same UPDATE that claims the quote for booking, so a price cannot be booked after it lapsed even if the page stayed open.
Claim the quote with a conditional UPDATE that moves it from quoted to booking only while it is unexpired, so a second click or a second tab finds no row to claim. Send a booking ID stored on the quote as the Idempotency-Key to the carrier, so a retry after a timeout returns the original carrier booking instead of creating a new one.
Copy them into your own object storage bucket keyed by booking ID as soon as the carrier makes them available, rather than linking to the carrier's portal. Serve them to shippers through short-lived signed download URLs, and keep the object key on the booking record.
Declare carrier API keys with secret(), a Postgres database with new SQLDatabase, a booking-confirmed topic with new Topic and a bills-of-lading bucket with new Bucket. A Subscription copies each bill of lading into the bucket, skipping the download when billsOfLading.exists() finds the file, and signedDownloadUrl() serves it.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.