@byteflow-solutions/sdk
v0.1.2
Published
Thin TypeScript SDK for the Byteflow data-service public REST API
Maintainers
Readme
@byteflow-solutions/sdk
Thin TypeScript SDK for the Byteflow data-service public REST API. Use this from Astro sites, widgets, and other external consumers instead of calling internal dashboard server functions.
Zero runtime dependencies — native fetch only.
Install
In this monorepo:
pnpm add @byteflow-solutions/sdkExternal Astro repos (once published):
npm install @byteflow-solutions/sdkEnvironment variables
| Variable | Required | Description |
|----------|----------|-------------|
| BYTEFLOW_ORG_ID | For site calls | Default organization id for the site |
The SDK ships with a default data-service URL (DEFAULT_BYTEFLOW_API_URL). You do not need to set BYTEFLOW_API_URL unless you are pointing at a non-production worker.
Do not expose BYTEFLOW_ORG_ID in client-side browser code for form or contact writes. Proxy those through a server route and collect Turnstile tokens in the browser only.
For event tracking, exposing the organization id in browser code is expected — the public ingest endpoint relies on origin checks and rate limits, not secrets. Analytics data may be noisy or spoofed; treat counts as directional, not authoritative.
Security model
Public SDK routes are protected server-side by:
- Organization id on every public read/write
- Origin/referer checks against the organization's configured site URL for browser requests
- Required Origin/Referer on
POST /events(browser-only ingest) - Rate limits per organization and IP, plus an org-wide cap on event ingest
- Turnstile validation on public form submissions when
TURNSTILE_SECRET_KEYis configured - 90-day retention on stored tracking events
The SDK does not ship secrets. Dashboard-only routes such as gallery upload/delete and places autocomplete live under /internal/* and require X-Byteflow-Internal-Secret.
Quick start
import { createByteflowSdk } from "@byteflow-solutions/sdk";
const sdk = createByteflowSdk({
organizationId: process.env.BYTEFLOW_ORG_ID,
});
// Server-to-server only
const { contact } = await sdk.contacts.create({
name: "Jane Doe",
email: "[email protected]",
source: "Zapier",
});
// Public website form submission
await sdk.forms.submit({
to: "[email protected]",
formId: "contact-form",
turnstileToken: formTurnstileToken,
message: {
name: "Jane Doe",
email: "[email protected]",
message: "I'd like a quote.",
},
});
// Build-time reads; Google place id is resolved server-side from the organization
const reviews = await sdk.reviews.list({ maxReviews: 5 });
const stats = await sdk.reviews.getStats({});
// Track a button click or any other custom event
await sdk.events.track({
eventKey: "button.click",
properties: {
buttonId: "hero-cta",
label: "Book now",
},
});Per-call organization override:
await sdk.forms.submit({
organizationId: "other-org-id",
to: "[email protected]",
formId: "contact-form",
message: { name: "Jane Doe" },
});Astro usage
Build-time reads (reviews, stats)
---
import { createByteflowSdk } from "@byteflow-solutions/sdk";
const sdk = createByteflowSdk({
organizationId: import.meta.env.BYTEFLOW_ORG_ID,
});
const reviews = await sdk.reviews.list({ maxReviews: 5 });
---
<ul>
{reviews.map((review) => (
<li>{review.authorName}: {review.rating}/5</li>
))}
</ul>Form writes (API route)
Never submit forms from the browser with a bare organization id. Proxy through an Astro API route and pass the Turnstile token from the site widget:
// src/pages/api/contact.ts
import type { APIRoute } from "astro";
import { createByteflowSdk } from "@byteflow-solutions/sdk";
const sdk = createByteflowSdk({
organizationId: import.meta.env.BYTEFLOW_ORG_ID,
});
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
await sdk.forms.submit({
to: import.meta.env.CLIENT_NOTIFY_EMAIL,
formId: "contact",
turnstileToken: body.turnstileToken,
message: body.message,
});
return new Response(null, {
status: 302,
headers: { Location: "/thanks" },
});
};Event tracking (browser button click)
Track custom events directly from the browser when the site origin matches the organization's configured URL:
---
import { createByteflowSdk } from "@byteflow-solutions/sdk";
const sdk = createByteflowSdk({
organizationId: import.meta.env.BYTEFLOW_ORG_ID,
});
---
<button id="hero-cta">Book now</button>
<script define:vars={{ organizationId: import.meta.env.BYTEFLOW_ORG_ID }}>
import { createByteflowSdk } from "@byteflow-solutions/sdk";
const sdk = createByteflowSdk({ organizationId });
const button = document.getElementById("hero-cta");
button?.addEventListener("click", () => {
void sdk.events.track({
eventKey: "button.click",
properties: {
buttonId: "hero-cta",
label: "Book now",
},
url: window.location.href,
path: window.location.pathname,
referrer: document.referrer || undefined,
});
});
</script>Use stable machine-readable eventKey values and put button-specific details in properties.
API surface
| SDK method | HTTP route | Notes |
|------------|------------|-------|
| contacts.create() | POST /contacts | Server-to-server only |
| forms.submit() | POST /submit-form | Requires Turnstile when enabled |
| events.track() | POST /events | Origin required; per-IP + org rate limits; browser-only |
| reviews.list() | GET /google-reviews | Requires organization id |
| reviews.getStats() | GET /google-reviews?statsOnly=true | Requires organization id |
The SDK uses organizationId in its public API. The data-service wire format still sends clientId for backward compatibility with existing routes.
Errors
Non-2xx responses throw ByteflowError with status, optional stable code, and payload:
import { ByteflowError } from "@byteflow-solutions/sdk";
try {
await sdk.forms.submit({ to: "x", formId: "contact", message: {} });
} catch (error) {
if (error instanceof ByteflowError) {
console.error(error.status, error.code, error.payload);
}
}Stable backend codes include INVALID_ORIGIN, ORIGIN_REQUIRED, RATE_LIMITED, BAD_TURNSTILE_TOKEN, and PLACE_NOT_CONFIGURED.
Custom fetch (Cloudflare service bindings)
The dashboard uses a service binding to reach data-service. Pass a custom fetch and optionally override baseUrl:
const sdk = createByteflowSdk({
baseUrl: env.DATA_SERVICE_URL, // optional; defaults to DEFAULT_BYTEFLOW_API_URL
fetch: (input, init) => env.DATA_SERVICE.fetch(input, init),
});Publish readiness checklist
Before publishing or rolling out to client Astro sites:
- [ ] Public routes in
apps/data-service/src/hono/app.tsare limited to contacts, submit-form, events, google-reviews, and read-only gallery routes - [ ] Gallery upload/delete and places autocomplete are only reachable under
/internal/* - [ ] Each organization has
organization.urlconfigured for origin checks - [ ]
INTERNAL_SERVICE_SECRETis set on both data-service and user-application workers - [ ]
TURNSTILE_SECRET_KEYis set on data-service when public forms are enabled - [ ]
RATE_LIMIT_KVis bound in production - [ ] Astro sites proxy writes through server routes and pass Turnstile tokens
- [ ]
pnpm run --filter @byteflow-solutions/sdk testpasses - [ ]
pnpm run --filter data-service testpasses
Build
pnpm run --filter @byteflow-solutions/sdk buildOutput is emitted to packages/sdk/dist.
