Atelier

Messaging

Every production SaaS sends mail. Welcome, receipt, password reset, trial-expiring, broadcast announcement, drip onboarding. Atelier ships the full pipeline — templates that inherit your design system, RLS-aware segments, multi-channel delivery — so you don’t bolt on Braze or Customer.io on top.

Send something

import { atelier } from '@atelier/sdk';
 
await atelier.send.email({
  to: user.email,
  template: 'welcome',
  data: { name: user.name, link: signupLink },
});

That’s the whole API for a transactional send. Pick a template, hand it data, ship it. Same shape for atelier.send.sms, atelier.send.push, and atelier.send.inApp.

Templates

Templates live in your project, branded with your design system automatically. Author them as React Email components or MJML; both inherit Design Cascade tokens so generated mail looks like your product, not a generic Mailchimp.

.atelier/messaging/
├── welcome.tsx
├── receipt.tsx
├── trial-expiring.tsx
└── _layout.tsx        ← shared header / footer / brand
// .atelier/messaging/welcome.tsx
import { Email, Heading, Text, Button } from '@atelier/messaging';
 
export default function Welcome({ name, link }: { name: string; link: string }) {
  return (
    <Email>
      <Heading>Welcome, {name}.</Heading>
      <Text>Two clicks and you're shipping.</Text>
      <Button href={link}>Open Studio</Button>
    </Email>
  );
}

Templates are versioned automatically — welcome@v1, welcome@v2. You can A/B variants from the Console without touching code.

Provider routing

Routing is per-channel and per-environment. Atelier-hosted is the default; bring your own when you need to.

// atelier.config.ts
export default {
  messaging: {
    email: {
      provider: 'atelier',  // 'atelier' | 'resend' | 'postmark' | 'sendgrid' | 'ses'
      from: 'noreply@your-app.com',
    },
    sms:   { provider: 'twilio' },
    push:  { provider: 'atelier' },
    inApp: { provider: 'atelier' },
  },
};

Same three-route logic as atelier.llm.*: dev sends through your local sandbox, prod sends through the configured provider, with optional fallback chains.

Segments

Segments are stored queries that respect Row Level Security. The signed-in admin gets the rows they’re allowed to see; everyone else sees only their tenant. No accidental cross-tenant blasts.

// .atelier/messaging/segments.ts
export const trialExpiringSoon = atelier.segment({
  name: 'Trial expiring in 3 days',
  source: () => atelier
    .from('subscriptions')
    .select('user_id')
    .eq('status', 'trial')
    .lte('expires_at', sql`NOW() + INTERVAL '3 days'`),
});
 
export const powerUsers = atelier.segment({
  name: 'Used the API last 7 days',
  source: () => atelier
    .from('api_events')
    .select('user_id, count(*)')
    .gte('at', sql`NOW() - INTERVAL '7 days'`)
    .groupBy('user_id')
    .having(sql`count(*) > 100`),
});

Segments are typed, testable, and live next to the rest of your project. The Console shows current size, recent recipients, and per-tenant distribution.

Broadcasts

A one-shot send to a segment. Pick a template, pick a segment, hit send.

await atelier.broadcast({
  segment: 'powerUsers',
  template: 'feature-launched',
  data: { feature: 'Branch deploys' },
  throttle: { perSecond: 50 },
});

A/B variants, send-time optimization, throttle, pause/resume, cancel — all from the Console too if you prefer clicking. Per-tenant broadcasts respect the segment’s RLS automatically.

Journeys

Multi-step lifecycle automation. Trigger → wait → branch → send → measure.

trigger: user.signed_up
  ↓ wait 1 hour
  ↓ send: welcome
  ↓ wait 3 days
  ↓ branch:
      ├── opened welcome?  → send: getting-started
      └── didn't?          → send: nudge
  ↓ wait 7 days
  ↓ branch:
      ├── made first deploy? → exit (won)
      └── didn't?            → send: book-a-call

Author journeys as code or in the visual builder. Both compile to the same definition — version-controlled with the rest of your project. Per-recipient state is visible in the Console (how far they’ve gotten, why a branch was taken).

Multi-channel

The same template can render across email, SMS, push, and in-app. Atelier picks the channel based on the recipient’s preferences and the channel’s nature.

await atelier.send.notify(user, {
  template: 'trial-expiring',
  channels: ['email', 'push', 'inApp'],   // try in order
  data: { daysLeft: 2 },
});

Channel-specific shapes are defined alongside the template:

export const sms = ({ daysLeft }: Props) =>
  `Your Atelier trial ends in ${daysLeft} days. Upgrade: app.atelyer.studio/billing`;
 
export const push = ({ daysLeft }: Props) => ({
  title: `${daysLeft} days left`,
  body: 'Keep your projects live — upgrade in one tap.',
});

Preferences center

A /preferences route is generated for every project. End-users opt in/out by channel and category (transactional, marketing, lifecycle). The UI inherits your design system; the underlying storage handles unsubscribes, double opt-in, and CCPA / GDPR compliance.

You can also drop the same component anywhere in your app:

import { PreferencesCenter } from '@atelier/messaging/react';
 
<PreferencesCenter user={currentUser} />

Inbound email

support@your-app.com (or any subdomain you control) routes into a Function. Reply in code, hand off to AI, route to a human.

// .atelier/functions/email/support.ts
export default async function inbound(email, ctx) {
  const { from, subject, text, attachments } = email;
  const ticket = await ctx.db.insert({
    table: 'tickets',
    row: { from, subject, body: text },
  });
 
  const reply = await ctx.llm.chat({
    model: 'medium',
    messages: [
      { role: 'system', content: 'You are a support agent for an invoicing SaaS.' },
      { role: 'user', content: text },
    ],
  });
 
  await ctx.email.reply(email, {
    body: reply.text,
    note: `Auto-replied. Ticket: ${ticket.id}`,
  });
}

DKIM and SPF are auto-configured when you verify the domain. Reply chains use the right In-Reply-To headers so threads stay threaded.

Tracking

Open pixels, click tracking, unsubscribe handling, bounce / complaint pipelines — wired by default. Aggregates surface in the Console; per-recipient timeline is one click away.

The same telemetry feeds back into Analytics, so the funnel from email_sent → email_opened → app_visit → first_deploy is one query.

In Functions

ctx.send mirrors the client SDK, scoped to the caller’s identity so RLS still holds.

export default async function trialNudge(_req, ctx) {
  const expiring = await atelier.segment('trialExpiringSoon').list();
  for (const user of expiring) {
    await ctx.send.email({
      to: user.email,
      template: 'trial-expiring',
      data: { daysLeft: user.daysLeft },
    });
  }
  return Response.json({ sent: expiring.length });
}

Pair with schedule: '0 9 * * *' in the frontmatter to run nightly.

See Production from minute 1 for the full inventory of what’s wired on day one.