Amazon SQS is a trademark of its respective owner, used here for identification and migration comparison only. No endorsement, partnership, or affiliation is implied.
SendMessage, long-poll with ReceiveMessage,
DeleteMessage when you’re done, and get at-least-once delivery with retries and dead-letter queues.
It’s battle-tested and the SDK is mature. But SQS is also a raw slice of an AWS account that sits
apart from everything else your agent touches:
- Every queue lives behind IAM credentials (
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYor a role), an AWS account, and CloudWatch — disconnected from wherever the agent’s email, cards, secrets, and KYC live. - The credential is coarse: a key with
sqs:*(or even a scoped policy) is an account-level grant. There is no per-end-user queue — you build tenant isolation yourself with naming conventions and hand-written IAM policies. - “Which customer’s agent enqueued that job, and what else can this agent touch?” is answered in IAM + CloudWatch for queues, and in unrelated systems for everything else. The queue has no shared accountability with the rest of the agent’s footprint.
queue primitive gives the agent the same capability — a
durable, at-least-once work queue (it’s Amazon SQS underneath) — but the queue is rooted in one
governed identity:
- The tenant user that owns the queue is the same user that owns its email inboxes, its cards, its vault secrets, and its KYC.
- The agent never holds a cloud key. Naive owns the underlying cloud credentials and scopes every queue to your tenant (one queue per resource, namespaced + tagged, with a queue policy locked to your tenant’s task role) — the same model as Compute.
- Whether an agent may use the queue at all is decided by that user’s Account Kit, and every request is metered against the tenant’s plan — so a leaked agent key can’t quietly run up a bill on your raw AWS account.
queue primitive, shows the smallest working swap, and
is explicit about the SQS features that don’t map yet.
Tested against: the AWS SDK for JavaScript v3
@aws-sdk/client-sqs v3.1080.x
(SQSClient, CreateQueueCommand, SendMessageCommand, ReceiveMessageCommand,
DeleteMessageCommand, GetQueueAttributesCommand, PurgeQueueCommand, DeleteQueueCommand;
Query/JSON protocol against https://sqs.<region>.amazonaws.com) and the Naive Node SDK
@usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1,
docs snapshot July 2026).Version notes:- SQS has two queue types: standard (best-effort ordering, at-least-once) and FIFO
(queue name ends
.fifo, exactly-once, ordered perMessageGroupId). Naive maps both:create({ type: "standard" })andcreate({ type: "fifo" }). - SQS dead-letter queues are configured with a
RedrivePolicyattribute pointing at a second queue’s ARN and amaxReceiveCount. Naive maps the DLQ with a single flag:create({ dlq: true })— butmaxReceiveCountand redrive config are not exposed. See what doesn’t map yet. - SQS identifies a queue by its
QueueUrl(per region/account). Naive identifies a queue by its id and resolves the region/account for you. - On Naive the queue is per-tenant:
naive.queue.*acts as the account’s default agent profile,naive.forUser(id).queue.*is a specific end-user. There is no SQS equivalent — tenant isolation in SQS is naming + IAM you write yourself.
Concept map
| Amazon SQS | Naive | Notes |
|---|---|---|
SQSClient behind AWS_ACCESS_KEY_ID / IAM role — one AWS account, shared across tenants | naive.forUser(id).queue — scopes every primitive per end-user | The core consolidation win |
| IAM credentials held by your code; the queue runs on your AWS trust | Agent never holds a cloud key — Naive owns the AWS creds, scopes to your tenant | Same model as Compute |
CreateQueueCommand({ QueueName }) → { QueueUrl } | naive.queue.create({ name }) → { id } | Standard queue |
CreateQueueCommand({ QueueName: "x.fifo", Attributes: { FifoQueue: "true" } }) | naive.queue.create({ name, type: "fifo" }) | FIFO queue |
RedrivePolicy attribute → separate DLQ + maxReceiveCount | naive.queue.create({ name, dlq: true }) | DLQ provisioned for you; maxReceiveCount not exposed — see gaps |
SendMessageCommand({ QueueUrl, MessageBody }) → { MessageId } | naive.queue.send(id, body) | Body is a string on both |
SendMessageCommand({ ..., MessageGroupId, MessageDeduplicationId }) | naive.queue.send(id, body, { group_id, dedup_id }) | FIFO ordering + dedup |
SendMessageCommand({ ..., DelaySeconds }) | naive.queue.send(id, body, { delay_seconds }) | Per-message delay |
ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages, WaitTimeSeconds }) → { Messages } | naive.queue.receive(id, { max, wait }) → { messages } | Long-poll; max ≤ 10, wait ≤ 20s |
ReceiveMessageCommand({ ..., VisibilityTimeout }) | naive.queue.receive(id, { visibility }) | In-flight visibility window |
message.Body / message.ReceiptHandle | m.body / m.receipt_handle | Per-message fields |
DeleteMessageCommand({ QueueUrl, ReceiptHandle }) | naive.queue.ack(id, receipt_handle) | Ack = delete after processing |
GetQueueAttributesCommand → ApproximateNumberOfMessages etc. | naive.queue.attributes(id) | Approximate depth / in-flight / delayed |
PurgeQueueCommand({ QueueUrl }) | naive.queue.purge(id) | Drop all messages |
DeleteQueueCommand({ QueueUrl }) | naive.queue.destroy(id) | Delete the queue (and its DLQ) |
ListQueuesCommand() → { QueueUrls } | naive.queue.list() | Queues for the identity |
GetQueueUrlCommand({ QueueName }) | naive.queue.get(id) | Fetch one queue |
MessageAttributes (typed key/value map alongside the body) | (none) | Encode metadata in the body — see gaps |
SendMessageBatchCommand / DeleteMessageBatchCommand (≤10 per call) | (none) | Loop send / ack — see gaps |
ChangeMessageVisibilityCommand (extend a single in-flight message) | (none) | Set visibility on receive; no per-message extend — see gaps |
SetQueueAttributes (retention, max size, KMS SSE, redrive tuning) | (managed) | Naive sets sane defaults; not tunable — see gaps |
| IAM policy / access policy scopes SQS in one AWS account | Account Kit gates queue per user + plan quota meters every call | Governance on the identity — see gains |
| CloudWatch metrics/logs for the queue | naive.forUser(id).logs.query() — one per-user timeline | Queue events beside email, cards, vault — see gain #3 |
AWS CLI aws sqs send-message | naive queue send <id> '{...}' | Full CLI parity — see CLI |
Before / after: the core path
The path that matters for almost every queue-backed agent is create a queue, enqueue work, long-poll for it, process, then delete the message. Here it is on both platforms.- No key, no region wrangling. SQS needs IAM credentials and a region; the queue runs on your
AWS trust. Naive runs the queue on cloud credentials it owns — the agent holds no cloud key and
never sees a
QueueUrl. - The queue is per-user, not per-account. SQS gives you one account; you hand-build tenant
isolation with naming + IAM. On Naive
naive.forUser(id).queueis the isolation boundary. ReceiptHandle→receipt_handle,Body→body. Field casing changes; the semantics (long-poll, visibility timeout, at-least-once, ack-to-delete) are the same SQS semantics.- The id is your identity, not a separate account. In SQS the key scopes SQS in one AWS
account. In Naive the same
forUser(id)handle also owns the agent’s email, cards, vault, and KYC.
FIFO, delay, and dead-letter queues
The other SQS features teams rely on — FIFO ordering, per-message delay, and a dead-letter queue — map directly, with less setup.- DLQ is a flag, not a two-queue dance. SQS makes you create the DLQ, read its ARN, and attach a
RedrivePolicy. Naive provisions the DLQ when you passdlq: trueand tears it down with the parent ondestroy. maxReceiveCountis not exposed on Naive — the redrive threshold is managed. If you tune it in SQS, note that as a gap.
Minimal viable migration
The smallest swap that keeps a working queue-backed agent alive is create → send → receive → ack.Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).
You can drop AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY and the region config for this path.Pick the identity that owns the queue
Use
naive.queue for the account’s default agent profile, or naive.forUser(id).queue to scope
the queue to a specific end-user. There is no QueueUrl and no region — Naive resolves them.Swap queue creation
Replace
CreateQueueCommand({ QueueName }) with naive.queue.create({ name }). For FIFO pass
type: "fifo"; for a dead-letter queue pass dlq: true (no separate DLQ + RedrivePolicy).
Keep the returned id in place of the QueueUrl.Swap send
Map
SendMessageCommand({ QueueUrl, MessageBody }) → naive.queue.send(id, body). Move
MessageGroupId / MessageDeduplicationId / DelaySeconds to the
{ group_id, dedup_id, delay_seconds } options object.Swap receive + ack
Map
ReceiveMessageCommand({ MaxNumberOfMessages, WaitTimeSeconds }) →
naive.queue.receive(id, { max, wait }), then read m.body / m.receipt_handle (lower-case).
Map DeleteMessageCommand({ ReceiptHandle }) → naive.queue.ack(id, receipt_handle).Consolidate further once you’re on Naive
This is where the migration pays for itself. In SQS, the IAM credential scopes queues in one AWS account and nothing else, and you build per-tenant isolation with naming conventions and policies. On Naive, the unit of isolation is a tenant user, and the queue is one of many primitives that identity owns.Gain #1 — one identity across primitives
- With SQS, the agent’s queues are a slice of one AWS account, and per-tenant isolation is a naming
convention plus IAM policy you write and audit yourself. With Naive,
naive.forUser(acme.id)is a single handle to queue and email and cards and vault and KYC. - Each queue is namespaced + tagged and locked to that tenant’s task role automatically — sibling tenants can never read each other’s queues, secrets, cards, or logs. Tear the customer down from one place.
Gain #2 — execution-time governance
- Whether an agent may use the queue at all is policy on the identity — not an IAM policy you
maintain in a second console. Toggle the
queueprimitive per user in the Account Kit, and every request is metered against the tenant’s plan quota.
- The agent’s code path stays the same for every tier —
client.queue.send(...),client.queue.receive(...). What changes at execution time:- Disabled primitive → the call is refused. If the Account Kit doesn’t grant
queue, the request never reaches AWS. - Quota exceeded →
429. Once usage crosses the plan’squeuequota for the period, the call is rejected with arate_limitederror — no surprise AWS bill from a runaway agent. - The agent holds no cloud key. Naive owns the AWS credentials; a leaked agent key can’t enumerate or drain queues on your raw AWS account.
- Disabled primitive → the call is refused. If the Account Kit doesn’t grant
Unlike money-moving primitives (cards,
trading), enqueuing a message is not approval-gated — there’s no
human-in-the-loop pause on
send. Queue governance is enable/disable + plan quota + unified
logs, enforced at execution time. If you need per-action human approval, that lives on the
sensitive primitives, not the queue.Gain #3 — unified accountability
- Every queue create, send, receive, and purge for a customer lands in one per-user activity log — alongside their email, card, vault, and KYC events, not in a separate CloudWatch namespace:
- That is the question that is hard to answer when queues live in SQS/CloudWatch, secrets live in your own manager, cards live in Stripe, and email lives somewhere else. Under Naive it is a single query.
What does not map yet
A migration guide that hides gaps is worse than none. The core path (create → send → receive → ack), FIFO, per-message delay, dead-letter queues, purge, attributes, and long-polling map cleanly — the queue is Amazon SQS underneath. But the following SQS capabilities have no direct equivalent on Naive’squeue primitive today. Check this list against your app before you commit.
| Amazon SQS feature | Status on Naive | Workaround |
|---|---|---|
MessageAttributes — typed metadata key/value map alongside the body | Not provided | Encode metadata inside the JSON body |
Batch APIs (SendMessageBatch / DeleteMessageBatch, ≤10 per call) | Not provided | Loop single send / ack calls |
ChangeMessageVisibility — extend a single in-flight message’s timeout | Not provided | Set visibility at receive time; size it for your slowest processing |
Redrive tuning (maxReceiveCount, RedrivePolicy, StartMessageMoveTask) | dlq: true only | DLQ is provisioned; the receive-count threshold is managed, not configurable |
| Queue attribute tuning (retention period, max message size, receive-wait default, KMS SSE) | Managed defaults | Not tunable per queue today |
Access policy / cross-account grants (AddPermission, SQS policy) | Not applicable | Naive owns the creds and scopes per tenant — that’s the point |
| Native SNS→SQS fan-out / EventBridge pipes / Lambda event-source mapping | Not provided | Poll the queue from a compute service worker |
| CloudWatch metrics / alarms | Per-user logs + attributes(id) | Approximate depth via attributes; events via the activity log |
Raw QueueUrl / ARN, region selection | Queue id only | Naive resolves region/account; you never handle a URL or ARN |
Where to go next
queueprimitive — standard / FIFO, DLQ, send / receive / ack, worker patternqueueSDK sub-client — typed method signaturesqueueCLI — create / send / receive / ack / attributes- Compute — pair a
serviceworker that long-polls the queue for a cheap autoscaling consumer - Customer billing — the plan + quota that meters queue usage at execution time
- Account Kits — the policy model that enables/disables the queue per user
- Tenant users — the identity that owns the queue, compute, email, cards, and vault