Norq

Java SDK

The norq Java package provides a client for sending and linting notification templates from Java applications.

Install

Coming soon. The norq package is not yet published to Maven Central. See norq.sh for updates.

The SDK runs entirely in-process — no norq binary or daemon is invoked at runtime.

Quick start

import sh.norq.Norq;
import sh.norq.Types.*;
import java.util.Map;
 
public class Example {
    public static void main(String[] args) {
        Norq norq = Norq.builder()
            .projectDir("./my-project")
            .build();
 
        SendMultiResult result = norq.send("transactional/welcome",
            new SendOpts()
                .to(new Recipient().email("alice@example.com"))
                .data(Map.of("user", Map.of("first_name", "Alice")))
        );
 
        for (SendResult r : result.getResults()) {
            System.out.printf("%s: success=%b%n", r.getChannel(), r.isSuccess());
        }
    }
}

Builder pattern

Norq norq = Norq.builder()
    .projectDir("./my-project")
    .strict(true)
    .recipientResolver(myResolver)
    .build();
Option Type Description
projectDir String Path to the norq project directory. Required — templates are read from this directory and compiled in-process.
strict boolean Reject sends that reference missing data or unknown pipes. Default true.
recipientResolver RecipientResolver Resolves user ID strings to Recipient objects

API

send(notification, opts)

Compile and deliver a notification.

SendMultiResult result = norq.send("transactional/welcome", new SendOpts()
    .to(new Recipient()
        .email("user@example.com")
        .cc(List.of("audit@example.com")))     // email-channel CC/BCC
    .data(Map.of("user", Map.of("first_name", "Gaurav")))
    .channels(List.of("email"))
    .dryRun(false)
    .idempotencyKey("order-shipped-ORD-123"));   // optional — see below
 
// result.getResults()  -> List<SendResult> (one per channel)
// result.getSkipped()  -> List<SkippedChannel> (channels that were skipped and why)

idempotencyKey(...) is forwarded to providers that honour it (Resend, SuprSend) as the Idempotency-Key: header. When null or empty, the SDK generates a UUID v4 per send and applies it to every channel; passing a stable user-derived key (order-shipped-ORD-123) extends dedup across retries and processes.

Recipient.cc(...) / bcc(...) apply to the email channel only; providers that support them (Resend) include them in the request body, others ignore.

SendOpts.attachments(List<Attachment>) accepts Attachment builders: new Attachment().filename("invoice.pdf").content(byte[]).contentType("application/pdf").contentId("logo").disposition("inline"). The SDK base64-encodes byte[] automatically. contentType is optional — falls back to filename-extension inference for the curated whitelist. Use disposition("inline") + contentId(...) to embed images, then reference them as <img src="cid:logo">. Provider caps: Resend 40 MB total, SendGrid 30 MB. See the Attachments reference for the full whitelist.

batch(notification, channel)

Create a batch for sending a single channel template to many recipients. The template is prepared once, then each add() call renders with per-recipient data. Requests are buffered and auto-flushed at the provider’s max batch size.

Batch batch = norq.batch("transactional/welcome", "email");
for (User user : users) {
    batch.add(user.getEmail(), Map.of("user", Map.of("first_name", user.getName())));
}
List<SendResult> results = batch.flush();

lint(notification)

List<LintResult> results = norq.lint(null);       // all
List<LintResult> results = norq.lint("transactional/welcome");  // one
 
for (LintResult result : results) {
    for (LintDiagnostic diag : result.getDiagnostics()) {
        System.out.printf("%s: %s [%s]%n",
            diag.getSeverity(), diag.getMessage(), diag.getRule());
    }
}

RecipientResolver

To send by user ID instead of explicit recipient details, configure a RecipientResolver:

Norq norq = Norq.builder()
    .projectDir("./my-project")
    .recipientResolver(userId -> {
        User user = db.getUser(userId);
        return new Recipient()
            .email(user.getEmail())
            .phone(user.getPhone());
    })
    .build();
 
norq.send("transactional/welcome",
    new SendOpts().to("user-123").data(Map.of("user", Map.of("first_name", "Alice"))));

Error handling

import sh.norq.NorqException;
 
try {
    norq.send("transactional/welcome", opts);
} catch (NorqException e) {
    System.err.println("Norq error: " + e.getMessage());
}

Sending push notifications

Push delivery is per-token. A PushRecipient holds every device the user has registered, possibly across ios, android, and web. The SDK fans the send out one HTTP call per token and emits one SendResult per token in result.getResults().

Token shape

PushRecipient push = new PushRecipient().tokens(List.of(
    new PushToken("fcm-android-token", "android"),
    new PushToken("apns-ios-token", "ios"),
    new PushToken(
        "https://updates.push.services.mozilla.com/wpush/v2/...",
        "web"
    ).keys(new PushVapidKeys("BNc...", "tBHI...")),
    new PushToken("ExponentPushToken[xxx]", "ios").provider("expo")
));
Field (builder) Required Notes
new PushToken(token, platform) yes platform is "ios", "android", or "web".
.provider(name) no Pin this token to a specific provider (e.g. "expo"); overrides routing.push.<platform>.
.keys(new PushVapidKeys(p256dh, auth)) iff web Captured from the browser’s PushSubscription.

APNs tokens additionally accept environment (sandbox | production) — pass it through the underlying JSON if you need per-token sandbox routing.

Mixed-platform recipient

SendMultiResult result = norq.send("transactional/order-shipped", new SendOpts()
    .to(new Recipient().push(push))
    .data(Map.of("user", Map.of("first_name", "Gaurav")))
    .channels(List.of("push")));

Per-token result fields

Each push SendResult carries two extra fields:

Field Meaning
getTokenIndex() Index into the original pushRecipient.getTokens() list.
getTokenStatus() Normalised cross-provider status: "invalid" (FCM UNREGISTERED, APNs BadDeviceToken, Web Push 404/410) or "transient" (5xx, rate limits). null on success.

Reaction guide

getTokenStatus() What it means What to do
"invalid" Token is permanently dead. Drop from DB; don’t retry.
"transient" Provider had a temporary failure. Retry with backoff; token still good.
null on success Delivery accepted. No action.
for (SendResult r : result.getResults()) {
    if (!"push".equals(r.getChannel())) continue;
    PushToken token = recipient.getPush().getTokens().get(r.getTokenIndex());
    if ("invalid".equals(r.getTokenStatus())) {
        db.deletePushToken(userId, token.getToken());
    } else if ("transient".equals(r.getTokenStatus())) {
        retryQueue.enqueue(userId, token, Instant.now().plusSeconds(60));
    }
}

See Push provider errors for the full set of per-token failure modes.

Codegen

norq codegen --lang java --out src/main/java/com/example/NorqTypes.java

Generates typed Java classes for each notification’s data shape.