Skip to main content
Cover image for Third-Party APIs That Power RoleReady

Third-Party APIs That Power RoleReady

saas integrations api nextjs typescript postgresql

I built RoleReady as a job tracking SaaS. Here are the third-party services I use and how they’re wired together.



Resend (Email)

Resend handles transactional emails. The SDK is straightforward:

import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: "RoleReady <noreply@roleready.me>",
  to: [user.email],
  subject: "Reset your password",
  html: emailTemplate,
});

I use it for password resets. Better Auth triggers the reset flow, and I pass the reset URL through a custom HTML template. The API returns a message ID on success, which I log for debugging.

Production uses the verified domain, dev uses Resend’s testing sandbox (onboarding@resend.dev).

Better Auth (Authentication)

Better Auth is an auth library for TypeScript. I chose it over NextAuth for better type safety and Drizzle integration.

The Drizzle adapter maps directly to my schema:

database: drizzleAdapter(db, {
  provider: "pg",
  schema: {
    user: schema.users,
    session: schema.sessions,
    account: schema.authAccounts,
    verification: schema.verifications,
  },
})

I use database hooks to create a default board for new users:

databaseHooks: {
  user: {
    create: {
      after: async (user) => {
        await db.insert(schema.boards).values({
          userId: user.id,
          name: "My Jobs",
          isDefault: true,
        });
      }
    }
  }
}

Google OAuth works out of the box with the social providers config. GitHub uses the genericOAuth plugin since it’s not a first-class provider. I fetch the user’s profile and emails from GitHub’s API and map the name field to firstName and lastName.

Jina AI (Web Scraping)

Job ingestion starts with a URL. Jina Reader converts any webpage to Markdown.

const JINA_READER_URL = "https://r.jina.ai/";

const response = await fetch(`${JINA_READER_URL}${encodeURIComponent(url)}`, {
  headers: {
    "Authorization": `Bearer ${process.env.JINA_API_READER_KEY}`,
    "X-Engine": "direct",
  }
});

const markdown = await response.text();

The direct engine skips some processing steps and returns faster. I cap the response at 500KB to avoid overwhelming the AI extraction. Before fetching, I validate the URL with url-sheriff for SSRF protection (blocking private IPs, localhost, cloud metadata services).

The Markdown goes to the AI pipeline for structured extraction.

OpenRouter (AI Inference)

OpenRouter provides access to many LLMs through a single API. I use it through BAML, which types my prompts and outputs.

The provider is configurable via AI_PROVIDER env var:

function resolveBamlClient(): BamlClientName {
  const provider = process.env.AI_PROVIDER?.trim().toUpperCase();

  if (!provider || provider === "OPENROUTER") {
    return "RoleReadyOpenRouter";
  }

  if (provider === "OPENAI") {
    return "RoleReadyOpenAI";
  }

  if (provider === "LOCAL") {
    return "RoleReadyLocal";
  }

  throw new Error(`Unsupported AI_PROVIDER: ${provider}`);
}

Default model is openrouter/aurora-alpha. I log metadata from each run (provider, model, prompt version, duration) for debugging and cost tracking.

The AI extracts job data (title, company, location, salary, requirements) and generates interview questions. Job rewrites stream token-by-token to the UI.

Neon (PostgreSQL)

Neon is a serverless Postgres provider. I use Drizzle ORM with the pg dialect.

Connection pooling is handled via a connection string. Neon’s autoscaling works well for sporadic workloads.

import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';

const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql);

I run migrations with bun x drizzle-kit push during development. The schema uses UUIDs for all primary keys, which Better Auth supports through its generateId config.

The Integration Flow

These services integrate cleanly. The flow for job ingestion is:

  1. User submits URL
  2. Jina fetches Markdown
  3. OpenRouter/BAML extracts structured data
  4. Neon stores the job
  5. Resend sends notifications if needed

Each service has a clear responsibility, and swapping any one of them would only require changes to the adapter layer.