MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Launching Gemini CLI extensions for Google Data Cloud

2025-12-10 23:24:16

In June, Google introduced Gemini CLI, an open-source AI agent that brings the power of Gemini directly into your terminal. We’re excited to announce open-source Gemini CLI extensions for Google Data Cloud services.

Building applications and analyzing trends with services like Cloud SQL, AlloyDB and BigQuery has never been easier — all from your local development environment! Whether you’re just getting started or a seasoned developer, these extensions make common data interactions such as app development, deployment, operations, and data analytics more productive and easier. So, let’s jump right in!

Using a Data Cloud Gemini CLI extension

Before you get started, make sure you have enabled the APIs and configured the IAM permissions required to access specific services.

To retrieve the newest functionality, install the latest release of the Gemini CLI (v0.6.0):

npm install -g @google/gemini-cli@latest

Next, install the extension:

gemini extensions install https://github.com/gemini-cli-extensions/<EXTENSION>

Replace with the name of the service you want to use. For example, alloydb, cloud-sql-postgresql or bigquery-data-analytics.

Before starting the Gemini CLI, you’ll need to configure the extension to connect with your Google Cloud project by adding the required environment variables. The table below provides more information on the configuration required.

Extension Name Description Configuration
alloydb Create resources and interact with AlloyDB for PostgreSQL databases and data. Configuration
alloydb-observability Monitor database performance and health for AlloyDB for PostgreSQL databases. Configuration
bigquery-data-analytics Discover and ask questions from BigQuery data. Configuration
bigquery-conversational-analytics Dive deeper, discover insights from BigQuery data using the built-in stateless agent offered by Conversational Analytics API. Configuration
cloud-sql-mysql Connect and interact with a Cloud SQL for MySQL database and data. Configuration
cloud-sql-mysql-observability Monitor database performance and health for Cloud SQL for MySQL databases. Configuration
cloud-sql-postgresql Connect and interact with a Cloud SQL for PostgreSQL database and data. Configuration
cloud-sql-postgresql-observability Monitor database performance and health for Cloud SQL for PostgreSQL databases. Configuration
cloud-sql-sqlserver Connect and interact with a Cloud SQL for SQL Server database and data. Configuration
cloud-sql-sqlserver-observability Monitor database performance and health for Cloud SQL for SQL Server databases. Configuration
dataplex Connect to Dataplex Universal Catalog to discover, manage, monitor, and govern data and AI artifacts across your data platform. Configuration
spanner Connect and interact with a Spanner database and data. Configuration
mcp-toolbox Load custom tools using MCP Toolbox for Databases. Configuration

Now, you can start the Gemini CLI using command gemini. You can view the extensions installed with the command /extensions


You can list the MCP servers and tools included in the extension using command /mcp list

Using the Gemini CLI for Cloud SQL for PostgreSQL extension

The Cloud SQL for PostgreSQL extension lets you perform a number of actions. Some of the main ones are included below:

  1. Create instance: Creates a new Cloud SQL instance for PostgreSQL (and also MySQL, or SQL Server)
  2. List instances: Lists all Cloud SQL instances in a given project
  3. Get instance: Retrieves information about a specific Cloud SQL instance
  4. Create user: Creates a new user account within a specified Cloud SQL instance, supporting both standard and Cloud IAM users

Curious about how to put it in action? Like any good project, start with a solid written plan of what you are trying to do. Then, you can provide that project plan to the CLI as a series of prompts, and the agent will start provisioning the database and other resources:



After configuring the extension to connect to the new database, the agent can generate the required tables based on the approved plan. For easy testing, you can prompt the agent to add test data.


Now the agent can use the context it has to generate an API to make the data accessible.


As you can see, these extensions make it incredibly easy to start building with Google Cloud databases!

Using the BigQuery Analytics extensions

For your analytical needs, we are thrilled to give you a first look at the Gemini CLI extension for BigQuery Data Analytics. We are also excited to give access to the Conversational Analytics API through the BigQuery Conversational Analytics extension. This is the first step in our journey to bring the full power of BigQuery directly into your local coding environment, creating an integrated and unified workflow.

With this extension you can

  1. Explore data: Use natural language to search for your tables.
  2. Analyze: Ask business questions on the data and generate intelligent insights.
  3. Dive deeper: Use conversational analytics APIs to dive deeper into the insights.
  4. And extend: Use other tools or extensions to extend into advanced workflows like charting, reporting, code management, etc.

This initial release provides a comprehensive suite of tools to Gemini CLI:

  • Metadata tools: Discover and understand the BigQuery data landscape.
  • Query execution tool: Run any BigQuery query and get the results back, summarized to your console.
  • AI-powered forecasting: Leverage BigQuery's built-in AI.Forecast function for powerful time-series predictions directly from the command line.
  • Deeper data Insights: The“ask_data_insights” tool provides access to server-side BigQuery agent for richer data insights.
  • And more…

[Note: To use the conversational analytics extension you need to enable additional APIs. Refer to documentation for additional info.]

Here is an example journey with analytics extensions:

Explore and analyze your data , e.g.,

> find tables related to PyPi downloads

✦ I found the following tables related to PyPi downloads:
​
   * file_downloads: projects/bigquery-public-data/datasets/pypi/tables/file_downloads
   * distribution_metadata: projects/bigquery-public-data/datasets/pypi/tables/distribution_metadata
>  Using bigquery-public-data.pypi.file_downloads show me top 10 downloaded pypi packages this month 
​
✦ Here are the top 10 most downloaded PyPI packages this month:
​
   1. boto3: 685,007,866 downloads
   2. botocore: 531,034,851 downloads
   3. urllib3: 512,611,825 downloads
   4. requests: 464,595,806 downloads
   5. typing-extensions: 459,505,780 downloads
   6. certifi: 451,929,759 downloads
   7. charset-normalizer: 428,716,731 downloads
   8. idna: 409,262,986 downloads
   9. grpcio-status: 402,535,938 downloads
   10. aiobotocore: 399,650,559 downloads

Run deeper insights

Using “ask_data_insights” to trigger an agent on the BigQuery (Conversational analytics API) to answer your questions. The server side agent is smart enough to gather additional context about your data and offer deeper insights into your questions.


You can go further and generate charts and reports by mixing BigQuery data with your local tools. Here’s a prompt to try:

”using bigquery-public-data.pypi.file_downloads can you forecast downloads for the last four months of 2025 for package urllib3? Please plot a chart that includes actual downloads for the first 8 months, followed by the forecast for the last four months”

Get started today!

Ready to level up your Gemini CLI extensions for our Data Cloud services? Read more in the extensions documentation. Check out our templates and start building your own extensions to share with the community!

Using Resend with Encore

2025-12-10 23:23:50

Every application needs to send emails. Welcome messages, password resets, notifications, receipts. But setting up SMTP servers, dealing with deliverability, and managing templates is tedious. Resend provides a modern API that makes email simple.

In this tutorial, we'll build email functionality using Resend. You'll learn how to send transactional emails, use React Email templates, track delivery status, handle webhooks, and store email history in a database.

What is Resend?

Resend is an email API built for developers. It provides:

  • Simple API - Send emails with a clean, modern interface
  • React Email - Build email templates with React components
  • Deliverability - Built-in best practices for inbox delivery
  • Webhooks - Real-time events for delivery, opens, clicks
  • Analytics - Track email performance and engagement
  • Domains - Verify and manage sending domains

Resend handles the complexity of email infrastructure so you can focus on your content.

What we're building

We'll create a backend with complete email functionality:

  • Transactional emails - Welcome emails, password resets, notifications
  • React Email templates - Type-safe, component-based email design
  • Email tracking - Auto-provisioned PostgreSQL database for email history
  • Webhook handling - Track delivery, bounces, and complaints
  • Batch emails - Send to multiple recipients efficiently
  • Domain verification - Set up custom sending domains

The backend will handle all email sending with delivery tracking and analytics.

Getting started


Prefer to skip the setup? Use encore app create --example=ts/resend to start with a complete working example. This tutorial walks through building it from scratch to understand each component.

First, install Encore if you haven't already:

# macOS
brew install encoredev/tap/encore

# Linux
curl -L https://encore.dev/install.sh | bash

# Windows
iwr https://encore.dev/install.ps1 | iex

Create a new Encore application. This will prompt you to create a free Encore account if you don't have one (required for secret management):

encore app create resend-app --example=ts/hello-world
cd resend-app

Setting up Resend

Creating your Resend account

  1. Go to resend.com and sign up for a free account
  2. Navigate to API Keys and create a new key
  3. Verify your domain (required for sending to real email addresses)

Resend offers a generous free tier with 100 emails per day and 3,000 per month.

Resend API Keys Dashboard

Domain verification

Important: To send emails to real recipients, you must verify your domain with Resend. Without domain verification, you can only send test emails to your own verified email address.

To verify your domain:

  1. Go to the Domains section in your Resend dashboard
  2. Click Add Domain and enter your domain (e.g., yourdomain.com)
  3. Add the DNS records (SPF, DKIM, DMARC) provided by Resend to your domain's DNS settings
  4. Wait for DNS propagation (usually 5-15 minutes)
  5. Click Verify in the Resend dashboard

Once verified, update your from addresses in the code to use your domain:

from: "[email protected]"  // Replace with your verified domain

For testing: If you don't have a domain yet, Resend lets you send test emails to your own verified email address using their sandbox domain ([email protected]). This is perfect for development and testing the integration.

Encore Domains Dashboard

Understanding API key permissions

When creating your API key, Resend offers two permission levels:

  • Full Access (full_access): Can create, delete, get, and update any resource. Use this for development and administrative tasks.
  • Sending Access (sending_access): Can only send emails. This is the recommended permission level for production applications following the principle of least privilege.

For production deployments, consider creating a sending access key and optionally restricting it to a specific domain. This limits the blast radius if your API key is ever compromised.

Installing the SDK

Install Resend and React Email:

npm install resend
npm install -D @react-email/components

Backend implementation

Creating the email service

Every Encore service starts with a service definition file (encore.service.ts). Services let you divide your application into logical components. At deploy time, you can decide whether to colocate them in a single process or deploy them as separate microservices, without changing a single line of code:

// email/encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("email");

Configuring Resend

Store your API key securely using Encore's built-in secrets management:

// email/resend.ts
import { Resend } from "resend";
import { secret } from "encore.dev/config";

const resendApiKey = secret("ResendApiKey");

export const resend = new Resend(resendApiKey());

Set your API key for local development:

# Development (use full access for testing)
encore secret set --dev ResendApiKey

# Production (use sending access for security)
encore secret set --prod ResendApiKey

Production Best Practice: Create a separate API key with sending_access permission for production. If you have multiple domains, create domain-specific keys to further isolate access. You can create domain-restricted keys using Resend's API:

// Example: Creating a domain-specific sending key (for admin tools)
const { data } = await resend.apiKeys.create({
  name: 'Production - yourdomain.com',
  permission: 'sending_access',
  domainId: 'your-domain-id', // Get this from Resend dashboard
});

Setting up the database

To track email history and delivery status, create a PostgreSQL database. With Encore, you can create a database by simply defining it in code. The framework automatically provisions the infrastructure locally using Docker.

Create the database instance:

// email/db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";

export const db = new SQLDatabase("email", {
  migrations: "./migrations",
});

Create the migration file:

-- email/migrations/1_create_emails.up.sql
CREATE TABLE emails (
  id TEXT PRIMARY KEY,
  resend_id TEXT UNIQUE,
  recipient TEXT NOT NULL,
  subject TEXT NOT NULL,
  template TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  delivered_at TIMESTAMP,
  opened_at TIMESTAMP,
  clicked_at TIMESTAMP,
  bounced_at TIMESTAMP,
  complained_at TIMESTAMP,
  error TEXT,
  metadata JSONB,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_emails_recipient ON emails(recipient, created_at DESC);
CREATE INDEX idx_emails_status ON emails(status);
CREATE INDEX idx_emails_resend_id ON emails(resend_id);

Creating email templates with React Email

React Email lets you build email templates using React components. Create a welcome email template:

// email/templates/welcome.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import * as React from "react";

interface WelcomeEmailProps {
  name: string;
  loginUrl: string;
}

export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our platform!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Welcome, {name}!</Heading>
          <Text style={text}>
            Thanks for joining us. We're excited to have you on board.
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={loginUrl}>
              Get Started
            </Button>
          </Section>
          <Text style={footer}>
            If you have any questions, just reply to this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "20px 0 48px",
  marginBottom: "64px",
};

const h1 = {
  color: "#333",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "40px 0",
  padding: "0",
  textAlign: "center" as const,
};

const text = {
  color: "#333",
  fontSize: "16px",
  lineHeight: "26px",
  textAlign: "center" as const,
};

const buttonContainer = {
  textAlign: "center" as const,
  margin: "32px 0",
};

const button = {
  backgroundColor: "#5469d4",
  borderRadius: "4px",
  color: "#fff",
  fontSize: "16px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "200px",
  padding: "12px",
  margin: "0 auto",
};

const footer = {
  color: "#8898aa",
  fontSize: "14px",
  lineHeight: "24px",
  textAlign: "center" as const,
  marginTop: "32px",
};

Sending emails

Create an endpoint to send welcome emails:

// email/send.ts
import { api } from "encore.dev/api";
import { resend } from "./resend";
import { db } from "./db";
import { render } from "@react-email/components";
import { WelcomeEmail } from "./templates/welcome";
import log from "encore.dev/log";

interface SendWelcomeEmailRequest {
  to: string;
  name: string;
  loginUrl?: string;
}

interface SendWelcomeEmailResponse {
  id: string;
  resendId: string;
}

export const sendWelcomeEmail = api(
  { expose: true, method: "POST", path: "/email/welcome" },
  async (req: SendWelcomeEmailRequest): Promise<SendWelcomeEmailResponse> => {
    const emailId = `email-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    log.info("Sending welcome email", { to: req.to, emailId });

    // Render the React Email template to HTML
    const html = render(
      WelcomeEmail({
        name: req.name,
        loginUrl: req.loginUrl || "https://yourapp.com/login",
      })
    );

    try {
      // Send email via Resend
      const { data, error } = await resend.emails.send({
        from: "[email protected]",
        to: req.to,
        subject: "Welcome to our platform!",
        html,
      });

      if (error) {
        throw new Error(`Failed to send email: ${error.message}`);
      }

      // Store email record
      await db.exec`
        INSERT INTO emails (id, resend_id, recipient, subject, template, status, metadata)
        VALUES (
          ${emailId},
          ${data!.id},
          ${req.to},
          ${"Welcome to our platform!"},
          ${"welcome"},
          ${"sent"},
          ${JSON.stringify({ name: req.name })}
        )
      `;

      log.info("Welcome email sent", { emailId, resendId: data!.id });

      return {
        id: emailId,
        resendId: data!.id, // Resend's unique email ID for tracking
      };
    } catch (error) {
      // Store failed email
      const errorMessage = error instanceof Error ? error.message : "Unknown error";

      await db.exec`
        INSERT INTO emails (id, recipient, subject, template, status, error)
        VALUES (
          ${emailId},
          ${req.to},
          ${"Welcome to our platform!"},
          ${"welcome"},
          ${"failed"},
          ${errorMessage}
        )
      `;

      throw error;
    }
  }
);

Password reset emails

Create another template for password resets:

// email/templates/password-reset.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import * as React from "react";

interface PasswordResetEmailProps {
  name: string;
  resetUrl: string;
}

export function PasswordResetEmail({ name, resetUrl }: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Password Reset</Heading>
          <Text style={text}>Hi {name},</Text>
          <Text style={text}>
            We received a request to reset your password. Click the button below to
            choose a new password:
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={resetUrl}>
              Reset Password
            </Button>
          </Section>
          <Text style={footer}>
            If you didn't request this, you can safely ignore this email.
          </Text>
          <Text style={footer}>This link will expire in 1 hour.</Text>
        </Container>
      </Body>
    </Html>
  );
}

// Styles similar to welcome email...
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "20px 0 48px",
  marginBottom: "64px",
};

const h1 = {
  color: "#333",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "40px 0",
  padding: "0",
  textAlign: "center" as const,
};

const text = {
  color: "#333",
  fontSize: "16px",
  lineHeight: "26px",
  textAlign: "left" as const,
  padding: "0 20px",
};

const buttonContainer = {
  textAlign: "center" as const,
  margin: "32px 0",
};

const button = {
  backgroundColor: "#dc3545",
  borderRadius: "4px",
  color: "#fff",
  fontSize: "16px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "200px",
  padding: "12px",
  margin: "0 auto",
};

const footer = {
  color: "#8898aa",
  fontSize: "14px",
  lineHeight: "24px",
  textAlign: "center" as const,
  marginTop: "16px",
};

And the endpoint:

// email/send.ts (continued)
import { PasswordResetEmail } from "./templates/password-reset";

interface SendPasswordResetRequest {
  to: string;
  name: string;
  resetUrl: string;
}

interface SendPasswordResetResponse {
  id: string;
  resendId: string;
}

export const sendPasswordReset = api(
  { expose: true, method: "POST", path: "/email/password-reset" },
  async (req: SendPasswordResetRequest): Promise<SendPasswordResetResponse> => {
    const emailId = `email-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    const html = render(
      PasswordResetEmail({
        name: req.name,
        resetUrl: req.resetUrl,
      })
    );

    const { data, error } = await resend.emails.send({
      from: "[email protected]",
      to: req.to,
      subject: "Reset your password",
      html,
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    await db.exec`
      INSERT INTO emails (id, resend_id, recipient, subject, template, status, metadata)
      VALUES (
        ${emailId},
        ${data!.id},
        ${req.to},
        ${"Reset your password"},
        ${"password-reset"},
        ${"sent"},
        ${JSON.stringify({ name: req.name })}
      )
    `;

    return {
      id: emailId,
      resendId: data!.id,
    };
  }
);

Handling webhooks

Resend sends webhooks for email events (delivered, opened, bounced, etc.). Handle these to update your database:

// email/webhooks.ts
import { api } from "encore.dev/api";
import { db } from "./db";
import log from "encore.dev/log";

interface ResendWebhookEvent {
  type: string;
  created_at: string;
  data: {
    email_id: string;
    from: string;
    to: string[];
    subject: string;
    created_at: string;
  };
}

export const handleWebhook = api.raw(
  { expose: true, path: "/webhooks/resend", method: "POST" },
  async (req, res) => {
    const event = (await req.json()) as ResendWebhookEvent;

    log.info("Received Resend webhook", { type: event.type, emailId: event.data.email_id });

    switch (event.type) {
      case "email.sent":
        await db.exec`
          UPDATE emails
          SET status = 'sent'
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.delivered":
        await db.exec`
          UPDATE emails
          SET status = 'delivered', delivered_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.opened":
        await db.exec`
          UPDATE emails
          SET opened_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.clicked":
        await db.exec`
          UPDATE emails
          SET clicked_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.bounced":
        await db.exec`
          UPDATE emails
          SET status = 'bounced', bounced_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.complained":
        await db.exec`
          UPDATE emails
          SET status = 'complained', complained_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;
    }

    res.writeHead(200);
    res.end();
  }
);

Configure the webhook URL in Resend Dashboard: https://your-domain.com/webhooks/resend

About Resend Email IDs: Every email sent through Resend gets a unique ID (like re_ABC123xyz). Store this ID in your database to:

  • Track email status through webhooks
  • Query individual email details via Resend's API
  • Debug delivery issues by cross-referencing with Resend's dashboard
  • Provide customer support with specific email references

Listing email history

Create an endpoint to retrieve sent emails:

// email/send.ts (continued)
interface EmailRecord {
  id: string;
  recipient: string;
  subject: string;
  template: string | null;
  status: string;
  deliveredAt: Date | null;
  openedAt: Date | null;
  clickedAt: Date | null;
  createdAt: Date;
}

interface ListEmailsRequest {
  recipient?: string;
  limit?: number;
}

interface ListEmailsResponse {
  emails: EmailRecord[];
}

export const listEmails = api(
  { expose: true, method: "GET", path: "/email/list" },
  async (req: ListEmailsRequest): Promise<ListEmailsResponse> => {
    const limit = req.limit || 50;

    let query;
    if (req.recipient) {
      query = db.query<{
        id: string;
        recipient: string;
        subject: string;
        template: string | null;
        status: string;
        delivered_at: Date | null;
        opened_at: Date | null;
        clicked_at: Date | null;
        created_at: Date;
      }>`
        SELECT id, recipient, subject, template, status, delivered_at, opened_at, clicked_at, created_at
        FROM emails
        WHERE recipient = ${req.recipient}
        ORDER BY created_at DESC
        LIMIT ${limit}
      `;
    } else {
      query = db.query<{
        id: string;
        recipient: string;
        subject: string;
        template: string | null;
        status: string;
        delivered_at: Date | null;
        opened_at: Date | null;
        clicked_at: Date | null;
        created_at: Date;
      }>`
        SELECT id, recipient, subject, template, status, delivered_at, opened_at, clicked_at, created_at
        FROM emails
        ORDER BY created_at DESC
        LIMIT ${limit}
      `;
    }

    const emails: EmailRecord[] = [];
    for await (const row of query) {
      emails.push({
        id: row.id,
        recipient: row.recipient,
        subject: row.subject,
        template: row.template,
        status: row.status,
        deliveredAt: row.delivered_at,
        openedAt: row.opened_at,
        clickedAt: row.clicked_at,
        createdAt: row.created_at,
      });
    }

    return { emails };
  }
);

Testing locally

Start your backend (make sure Docker is running first):

encore run

Your API is now running locally. Open the local development dashboard at http://localhost:9400 to explore your API.

Send a welcome email

curl -X POST http://localhost:4000/email/welcome \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "name": "John Doe",
    "loginUrl": "https://yourapp.com/login"
  }'

Note: Replace [email protected] with the email address you used to sign up for Resend, or any email address on your verified domain.

Response:

{
  "id": "email-1234567890-abc",
  "resendId": "re_ABC123xyz"
}

Send a password reset email

curl -X POST http://localhost:4000/email/password-reset \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "name": "John Doe",
    "resetUrl": "https://yourapp.com/reset/token123"
  }'

List sent emails

# All emails
curl http://localhost:4000/email/list

# For specific recipient
curl "http://localhost:4000/email/[email protected]"

Exploring the local dashboard

The local development dashboard at http://localhost:9400 provides:

  • API Explorer - Test email sending interactively
  • Service Catalog - Auto-generated API documentation
  • Architecture Diagram - Visual representation of your services
  • Distributed Tracing - See the full flow including Resend API calls
  • Database Explorer - Browse email history, delivery status, and engagement

Encore distributed tracing showing email sending flow

The database explorer shows all your email records with delivery tracking:

Encore database explorer showing email history

Advanced features

Additional email fields

Resend supports many additional email fields beyond the basics. Here's how to use CC, BCC, reply-to addresses, tags, and headers:

export const sendAdvancedEmail = api(
  { expose: true, method: "POST", path: "/email/advanced" },
  async (req: {
    to: string[];
    subject: string;
    html: string;
  }) => {
    const { data, error } = await resend.emails.send({
      from: "[email protected]",
      to: req.to,
      subject: req.subject,
      html: req.html,
      // CC and BCC recipients
      cc: ["[email protected]"],
      bcc: ["[email protected]"],
      // Reply-to address (different from 'from')
      replyTo: "[email protected]",
      // Tags for filtering and analytics
      tags: [
        { name: "category", value: "transactional" },
        { name: "priority", value: "high" },
      ],
      // Custom headers
      headers: {
        "X-Entity-Ref-ID": "123456",
      },
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    return { id: data!.id };
  }
);

Use cases for these fields:

  • CC/BCC: Copy managers on customer communications, archive all emails to a compliance inbox
  • Reply-To: Direct replies to support@ even when sending from noreply@ or automated systems
  • Tags: Filter and analyze emails in Resend dashboard by campaign, user segment, or priority
  • Headers: Add custom tracking IDs, reference numbers, or metadata for your internal systems

Batch emails

Send to multiple recipients efficiently:

export const sendBatch = api(
  { expose: true, method: "POST", path: "/email/batch" },
  async (req: {
    recipients: Array<{ email: string; name: string }>;
    subject: string;
    html: string;
  }) => {
    const emails = req.recipients.map((recipient) => ({
      from: "[email protected]",
      to: recipient.email,
      subject: req.subject,
      html: req.html,
    }));

    const { data, error } = await resend.batch.send(emails);

    if (error) {
      throw new Error(`Batch send failed: ${error.message}`);
    }

    // Store each email in database
    for (let i = 0; i < data!.data.length; i++) {
      const emailId = `email-${Date.now()}-${i}`;
      await db.exec`
        INSERT INTO emails (id, resend_id, recipient, subject, status)
        VALUES (
          ${emailId},
          ${data!.data[i].id},
          ${req.recipients[i].email},
          ${req.subject},
          ${"sent"}
        )
      `;
    }

    return { sent: data!.data.length };
  }
);

Email with attachments

Send emails with file attachments:

export const sendWithAttachment = api(
  { expose: true, method: "POST", path: "/email/attachment" },
  async (req: {
    to: string;
    subject: string;
    html: string;
    attachment: {
      filename: string;
      content: string; // Base64 encoded
    };
  }) => {
    const { data, error } = await resend.emails.send({
      from: "[email protected]",
      to: req.to,
      subject: req.subject,
      html: req.html,
      attachments: [
        {
          filename: req.attachment.filename,
          content: Buffer.from(req.attachment.content, "base64"),
        },
      ],
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    return { id: data!.id };
  }
);

Scheduled emails

Use Encore's cron jobs to send scheduled emails:

import { CronJob } from "encore.dev/cron";

const _ = new CronJob("weekly-digest", {
  title: "Send weekly digest emails",
  every: "0 9 * * 1", // Every Monday at 9 AM
  endpoint: sendWeeklyDigest,
});

export const sendWeeklyDigest = api(
  { expose: false },
  async (): Promise<void> => {
    // Fetch users who want weekly digests
    // Send digest email to each user
    log.info("Sending weekly digest emails");
  }
);

Email analytics

Track email performance:

interface EmailAnalyticsResponse {
  totalSent: number;
  delivered: number;
  opened: number;
  clicked: number;
  bounced: number;
  deliveryRate: number;
  openRate: number;
  clickRate: number;
}

export const getAnalytics = api(
  { expose: true, method: "GET", path: "/email/analytics" },
  async (): Promise<EmailAnalyticsResponse> => {
    const stats = await db.queryRow<{
      total: number;
      delivered: number;
      opened: number;
      clicked: number;
      bounced: number;
    }>`
      SELECT
        COUNT(*) as total,
        COUNT(CASE WHEN status = 'delivered' THEN 1 END) as delivered,
        COUNT(CASE WHEN opened_at IS NOT NULL THEN 1 END) as opened,
        COUNT(CASE WHEN clicked_at IS NOT NULL THEN 1 END) as clicked,
        COUNT(CASE WHEN status = 'bounced' THEN 1 END) as bounced
      FROM emails
      WHERE created_at > NOW() - INTERVAL '30 days'
    `;

    const total = Number(stats!.total);
    const delivered = Number(stats!.delivered);
    const opened = Number(stats!.opened);
    const clicked = Number(stats!.clicked);

    return {
      totalSent: total,
      delivered,
      opened,
      clicked,
      bounced: Number(stats!.bounced),
      deliveryRate: delivered / total,
      openRate: opened / delivered,
      clickRate: clicked / opened,
    };
  }
);

Use cases

User onboarding flow

Send a series of onboarding emails:

export const startOnboarding = api(
  { expose: true, method: "POST", path: "/onboarding/start" },
  async (req: { email: string; name: string }) => {
    // Send immediate welcome email
    await sendWelcomeEmail({
      to: req.email,
      name: req.name,
    });

    // Schedule follow-up emails using your job queue
    // Day 1: Getting started tips
    // Day 3: Feature highlights
    // Day 7: Success stories

    return { started: true };
  }
);

Order confirmations

Send transactional receipts:

export const sendOrderConfirmation = api(
  { expose: true, method: "POST", path: "/email/order" },
  async (req: {
    to: string;
    orderId: string;
    items: Array<{ name: string; price: number }>;
    total: number;
  }) => {
    const html = render(
      OrderConfirmationEmail({
        orderId: req.orderId,
        items: req.items,
        total: req.total,
      })
    );

    const { data } = await resend.emails.send({
      from: "[email protected]",
      to: req.to,
      subject: `Order confirmation - ${req.orderId}`,
      html,
    });

    return { sent: true, id: data!.id };
  }
);

Notification digests

Aggregate notifications into daily/weekly emails:

export const sendDigest = api(
  { expose: true, method: "POST", path: "/email/digest" },
  async (req: {
    to: string;
    notifications: Array<{ title: string; message: string; url: string }>;
  }) => {
    const html = render(
      DigestEmail({
        notifications: req.notifications,
      })
    );

    await resend.emails.send({
      from: "[email protected]",
      to: req.to,
      subject: `You have ${req.notifications.length} new notifications`,
      html,
    });

    return { sent: true };
  }
);

Frontend integration

From your frontend, trigger email sends:

// Example React component
import { useState } from "react";

function PasswordResetForm() {
  const [email, setEmail] = useState("");
  const [sent, setSent] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await fetch("http://localhost:4000/email/password-reset", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        to: email,
        name: "User", // Get from your user database
        resetUrl: `https://yourapp.com/reset/${generateToken()}`,
      }),
    });

    setSent(true);
  };

  if (sent) {
    return <p>Check your email for reset instructions!</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
      />
      <button type="submit">Reset Password</button>
    </form>
  );
}

For complete frontend integration guides, see the frontend integration documentation.

Deployment

Self-hosting

See the self-hosting instructions for how to use encore build docker to create a Docker image and configure it.

Encore Cloud Platform

Deploy your application using git push encore:

git add -A .
git commit -m "Add Resend email functionality"
git push encore

Set your production secret:

encore secret set --prod ResendApiKey

Note: Encore Cloud is great for prototyping and development with fair use limits. For production workloads, you can connect your AWS or GCP account and Encore will provision infrastructure directly in your cloud account.

Next steps

If you found this tutorial helpful, consider starring Encore on GitHub to help others discover it.

✨ How to Create SVGs in Figma and Animate Them Using Motion 🚀

2025-12-10 23:22:26

SVGs are one of the most powerful tools in modern UI development. They’re scalable, lightweight, customizable, and—best of all—easy to animate when paired with Motion.

In this guide, I’ll walk you through how to:

  • Design SVGs in Figma
  • Export them cleanly
  • Convert them into React components
  • Animate them with Motion (Framer Motion)

Let’s jump right in.

Open Figma and create a new frame.

Creating SVG lines is simple — select the Pen tool, click to set your starting point, and then click again where you want the line to end. Figma will automatically create a clean vector path that you can later export as an SVG

To create curves, we’ll use the Bend tool.

Select your SVG line, click on the Bend tool, and then click on the line again. You’ll see two Dots appear, just drag those dots to shape the curve however you like. That’s it! You now have a clean, smooth, curved line.

Once your line looks good, exporting it as an SVG is easy. Just select the line, right-click, and choose Copy as SVG.

I’ve already created a demo component with some icons and a few pre-built SVG lines animating in the background. Now, let’s take a look at how we can animate the left SVG line using Motion.


"use client";

import MotionDiv from "@/components/animation/motion-div";
import { cn } from "@/lib/utils";
import React from "react";
import { motion } from "motion/react";
import { DotPattern } from "@/components/ui/dot-pattern";
import Container from "@/components/container";
import ThemeToggle from "@/components/theme-toggle";
import Scales from "@/components/scales";

const Demo = () => {
  return (
    <Container className="relative flex h-screen max-w-[524px] flex-col items-center justify-center bg-white dark:bg-black">
      <div className="mb-4">
        <ThemeToggle />
      </div>
      <Scales />
      <div className="relative overflow-hidden border-y">
        <Heading
          heading="Multi-Model Orchestration"
          subHeading="Use different AI models together in a single flow to optimize accuracy, creativity, and performance across tasks like generation, enhancement, and analysis."
        />
        <div className="flex scale-90 flex-col items-center justify-center sm:scale-105 md:scale-110 lg:scale-125">
          <MotionDiv className="relative flex h-72 w-[484px] scale-80 items-center justify-center bg-white p-2 px-4 sm:px-0 dark:bg-black">
            <DotPattern
              className={cn(
                "[mask-image:radial-gradient(150px_circle_at_center,white,transparent)]",
                "text-neutral-400 dark:text-neutral-700",
              )}
            />

            <IconCard className="left-8.5" icon={<GeminiIcon />} />
            <IconCard className="left-25.5" icon={<OpenAiIcon />} />
            <IconCard className="left-42.5" icon={<DeepSeekIcon />} />
            <IconCard className="right-42.5" icon={<ClaudeIcon />} />
            <IconCard className="right-25.5" icon={<GrokIcon />} />
            <IconCard className="right-8.5" icon={<QwenAiIcon />} />

            <Line1 />
            <Line2 />
            <Line3 />
            <Line4 />
            <Line5 />
            <Line6 />
            <MotionDiv
              initial={{}}
              className="bg-card absolute right-1/2 bottom-6 flex size-14.5 translate-x-1/2 items-center justify-center rounded-full border p-1"
            >
              <div className="flex size-full items-center justify-center rounded-full p-2.5 text-center shadow-2xl">
                SM
              </div>
            </MotionDiv>
          </MotionDiv>
        </div>
      </div>
    </Container>
  );
};

export default Demo;

type Heading = {
  heading: string;
  subHeading: string;
};

const Heading = ({ heading, subHeading }: Heading) => {
  return (
    <div className="mb-2 flex flex-col items-start justify-center gap-0.5 px-6 pt-8 sm:mb-8 lg:pt-4">
      <h2 className="text-card-title text-base font-medium tracking-tight sm:text-lg">
        {heading}
      </h2>
      <p className="text-paragraph mt-1 max-w-lg text-xs leading-snug font-light text-pretty sm:text-sm">
        {subHeading}
      </p>
    </div>
  );
};

type IconCardType = {
  className: string;
  icon: React.ReactNode;
};

const IconCard = ({ className, icon }: IconCardType) => {
  return (
    <MotionDiv
      className={cn(
        `bg-card absolute top-10 z-10 flex w-fit items-center justify-center rounded-full border shadow-2xl inset-shadow-sm`,
        className,
      )}
    >
      <div className="flex size-9.5 items-center justify-center rounded-full p-1.5">
        {icon}
      </div>
    </MotionDiv>
  );
};


const Line2 = () => {
  return (
    <svg
      width="107"
      height="166"
      viewBox="0 0 107 166"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-30"
      stroke="url(#line-2)"
    >
      <path strokeWidth={2} d="M106.5 166C106.5 91.8654 0.5 93.6701 0.5 0" />
      <defs>
        <motion.linearGradient
          id="line-2"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "100%",
            y1: "90%",
            y2: "115%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}
        >
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#10b981" />
          <stop offset="0.66" stopColor="#0891b2" />
          <stop offset="0.1" stopColor="#eeeeee" />
        </motion.linearGradient>
      </defs>
    </svg>
  );
};
const Line3 = () => {
  return (
    <svg
      width="50"
      height="166"
      viewBox="0 0 50 166"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-47"
      stroke="url(#line-3)"
    >
      <path strokeWidth={2} d="M49.5 166C49.5 93.9514 0.5 93.9514 0.5 0" />
      <defs>
        <motion.linearGradient
          id="line-3"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "100%",
            y1: "90%",
            y2: "115%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}
        >
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#10b981" />
          <stop offset="0.66" stopColor="#0891b2" />
          <stop offset="0.1" stopColor="#eeeeee" />
        </motion.linearGradient>
      </defs>
    </svg>
  );
};

const Line4 = () => {
  return (
    <svg
      width="50"
      height="166"
      viewBox="0 0 50 166"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 right-47 rotate-y-180"
      stroke="url(#line-4)"
    >
      <path strokeWidth={2} d="M49.5 166C49.5 93.9514 0.5 93.9514 0.5 0" />
      <defs>
        <motion.linearGradient
          id="line-4"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "100%",
            y1: "90%",
            y2: "115%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}
        >
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#10b981" />
          <stop offset="0.66" stopColor="#0891b2" />
          <stop offset="0.1" stopColor="#eeeeee" />
        </motion.linearGradient>
      </defs>
    </svg>
  );
};
const Line5 = () => {
  return (
    <svg
      width="107"
      height="166"
      viewBox="0 0 107 166"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 right-30 rotate-y-180"
      stroke="url(#line-2)"
    >
      <path strokeWidth={2} d="M106.5 166C106.5 91.8654 0.5 93.6701 0.5 0" />
      <defs>
        <motion.linearGradient
          id="line-2"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "100%",
            y1: "90%",
            y2: "115%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}
        >
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#10b981" />
          <stop offset="0.66" stopColor="#0891b2" />
          <stop offset="0.1" stopColor="#eeeeee" />
        </motion.linearGradient>
      </defs>
    </svg>
  );
};
const Line6 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 right-13 rotate-y-180"
    >
      <path
        strokeWidth={2}
        stroke="url(#line-6)"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      />

      <defs>
        <motion.linearGradient
          id="line-6"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "100%",
            y1: "90%",
            y2: "115%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}
        >
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#10b981" />
          <stop offset="0.66" stopColor="#0891b2" />
          <stop offset="0.1" stopColor="#eeeeee" />
        </motion.linearGradient>
      </defs>
    </svg>
  );
};


Let’s take the SVG we created in Figma and use it here.
First, we’ll turn it into a component called Line1.

Here’s what the component looks like.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      <path
        strokeWidth={2}
        stroke="#eeeeee"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      />
    </svg>
  );
};

Now, to animate the line, we’ll use Motion with a linear gradient.
To do that, we need to place our gradient inside a <defs> section, which is where SVG stores reusable definitions.

Next, we’ll turn the linear gradient into a Motion element and give it an ID so we can reference it from the SVG stroke.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      <path
        strokeWidth={2}
        stroke="url(#line-1)"  // use the id here
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      />

<defs>
<motion.linearGradient
id="line-1"
>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

A <stop> defines one color point inside a gradient.
A gradient is essentially a list of stops that blend seamlessly into one another.

stopColor is the actual color for that stop.

offset defines where the color appears in the gradient.

It is like a progress percentage:

  • 0 or 0% → start of the gradient
  • 1 or 100% → end of the gradient
  • 0.5 or 50% → middle

Inside the linear gradient, we have four values: x1 and x2 control how the gradient moves along the x-axis, and y1 and y2 control movement along the y-axis.

For this example, let’s start by animating x1 and x2.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      <path
        strokeWidth={2}
        stroke="url(#line-1)"  // use the id here
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      />

<defs>
<motion.linearGradient
id="line-1"
          initial={{
            x1: "0%",
            x2: "10%",
          }}

>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

In the original gradient, x1 = 0% and x2 = 10% define where the red/blue part of the gradient starts.

Now, you can probably guess what we’re going to do
we’ll animate those same values so the gradient moves across the line.
So instead of starting at 0% → 10%, we’ll animate them to 90% → 100%.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      <path
        strokeWidth={2}
        stroke="url(#line-1)"  // use the id here
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      />

<defs>
<motion.linearGradient
id="line-1"
          initial={{
            x1: "0%",
            x2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "110%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}

>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

we’re basically telling the gradient to slide forward along the line, creating that smooth glowing effect.

The gradient starts at its initial position and then moves from 90% to 110%, looping continuously. The linear easing keeps the motion steady, while the repeat delay adds a small pause before the next cycle, making the animation feel more natural and less aggressive.

Let's see it in action

But as you can see, the gradient doesn’t start from the top, it appears somewhere in the middle.

That’s happening because we’re only animating the x-axis values, not the y-axis. The gradient is technically moving, but only horizontally, so the effect doesn’t align with the shape of the curve.

To understand this better, we can use a simple <rect> example. A behaves similarly to a <path>, but because it’s a clean, straight shape, it makes the gradient behavior much easier to see.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      {/* <path
        strokeWidth={2}
        stroke="url(#line-1)"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      /> */}

      <rect
        width="169"
        height="167"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
        fill="url(#line-1)"
      />

<defs>
<motion.linearGradient
id="line-1"
          initial={{
            x1: "0%",
            x2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "110%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}

>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

So now let’s also animate the y-axis values.
By adjusting y1 and y2 along with the x-axis, the gradient will follow the full direction of the line instead of cutting through the middle.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      {/* <path
        strokeWidth={2}
        stroke="url(#line-1)"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      /> */}

      <rect
        width="169"
        height="167"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
        fill="url(#line-1)"
      />

<defs>
<motion.linearGradient
id="line-1"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "110%",
            y1: "90%",
            y2: "110%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}

>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

Now just remove the and switch back to your , and you’ll have a clean, modern linear-gradient animation flowing perfectly across your SVG line.

const Line1 = () => {
  return (
    <svg
      width="169"
      height="167"
      viewBox="0 0 169 167"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute top-19 left-13"
    >
      <path
        strokeWidth={2}
        stroke="url(#line-1)"
        d="M168.499 166.026C168.499 99.7414 5.52495 96.283 0.499313 0.0260773"
      /> 
<defs>
<motion.linearGradient
id="line-1"
          initial={{
            x1: "0%",
            x2: "10%",
            y1: "0%",
            y2: "10%",
          }}
          animate={{
            x1: "90%",
            x2: "110%",
            y1: "90%",
            y2: "110%",
          }}
          transition={{
            duration: 0.8,
            repeat: Infinity,
            repeatDelay: 1,
            repeatType: "loop",
            ease: "linear",
          }}

>
          <stop stopColor="#eeeeee" />
          <stop offset="0.33" stopColor="#dc2626" />
          <stop offset="0.66" stopColor="#3b82f6" />
          <stop offset="1" stopColor="#eeeeee" />
</motion.linearGradient>
</defs>
    </svg>
  );
};

## End Result:

And that’s it — we’ve learned how to create an SVG in Figma, export it, and animate it using Motion.

If you found this helpful, follow me on Twitter for more tips, breakdowns, and behind-the-scenes builds.

I’m also launching my template soon, so stay tuned — you won’t want to miss it! 🚀

I Built a Local-First Secret Manager Because I Don't Trust AI Agents With My .env Files

2025-12-10 23:22:23

Why I Created It

Two months ago, someone replied to my tweet asking for ideas for new software.
The reply briefly mentioned the need for a local-based secret manager, which piqued my interest and led me to start researching.

There, I read a surprising article.
It was about how AI agents, under the guise of helping users during development or due to manipulated web search results, could leak .env files.
Security companies like PromptArmor and Mindgard have documented how AI agents can be exploited to extract information from accessible files.

So I decided to create a secret manager that prevents AI agents from viewing .env files and bypassing commands.

The Unspoken Problem with .env Files

Honestly, most people store secrets in plaintext .env files.
It's convenient, it works, and that's how it's always been done.
But because it's plaintext, any process with access to the file can send it somewhere, accidentally commit it to Git, or, as mentioned earlier, an AI could unintentionally leak it.
Additionally, since .env is located in the project directory, it's hard to manage centrally.

Why Not Use Doppler or AWS Secrets Manager?

Doppler is great when working in a team and needing collaboration features.
AWS Secrets Manager is perfect if you're already deep into the AWS ecosystem.

But I'm not part of a team, I don't use AWS, and I prefer offline solutions.
SaaS-based tools like Doppler even require monthly payments.

And there's one key difference between my tool and others: it requires user approval to use secrets.
I built a per-request approval system, not just per-user or per-process (similar to how 1Password's SSH feature works).

Development Principles

Encryption
All secrets are encrypted with AES-256-GCM before touching disk.
The encryption key is derived from the master password using PBKDF2 with 100,000 iterations.

Easy Migration
I designed it to be easy from the start and created guides so you don't have to spend much time learning a new tool.

Lessons Learned While Building

Unexpected Age Barrier
Something I didn't expect: to properly distribute a macOS app, you need to pay $99 annually and join Apple's Developer Program.
I think this is an important security measure for macOS, even if it's inconvenient.
But what might be a bigger issue—and perhaps unique to me—is that you must be at least 19 years old to join.

I'm under 19. So LocalKeys only worked on my Mac.
Others couldn't run it on their Macs without security warnings.
It was frustrating, but it was a good lesson in understanding platform requirements upfront.

Since I couldn't find a proper solution, I'm temporarily showing users how to remove the quarantine attribute after download.
It's not ideal, but it's the only option until I become an adult and join the developer program.

Real User Feedback
After launch, I posted about LocalKeys on Reddit. Someone pointed out, "How do you use the same secrets across multiple devices? Most people use different computers at work and home."

A valid point. I was so focused on "local-first" that I overlooked the multi-device workflow many developers need.
I've now added optional encrypted synchronization to the roadmap, maintaining the local-first philosophy while adding convenience when needed.

The Result: LocalKeys

After two months of development and real-world testing, I launched LocalKeys.
LocalKeys is open source, anyone can build it, it's a one-time purchase, and it's simple.
You can check it out at localkeys.privatestater.com

Is This for You?

Good cases for using LocalKeys:

  • Working on personal/indie projects with sensitive credentials
  • Want full control over your data
  • Need offline access or run behind restrictive firewalls
  • Prefer a simpler, more focused tool over enterprise solutions

When LocalKeys isn't a good fit:

  • You need team collaboration features right now
  • Want ready-to-use cloud synchronization
  • Require enterprise audit logs
  • Are already satisfied with Doppler/1Password CLI, etc.

Planned Features

  • Linux support
  • Optional encrypted synchronization between devices
  • More language integrations based on feedback

I'd Love Your Feedback

This is my first desktop software, and I'm sure there are things I haven't considered.

  1. What other secret management issues are you facing?
  2. Do you prefer an offline-first approach, or do you favor cloud-based solutions?
  3. For those working across multiple devices, how do you currently handle credential synchronization?
  4. Does the concern about "unauthorized access even when encrypted" resonate with you?

As an indie developer, building publicly is both scary and exciting.
If you've faced similar security concerns or have thoughts on the approach, I'd really appreciate it.

And if you try LocalKeys, please let me know what breaks!

Note: This isn't to say cloud solutions are bad. They're great for team or enterprise use cases. This is just a different approach for different needs.

Host A Static Website in AWS S3 and CloudFront (Using Terraform)

2025-12-10 23:21:59

Hosting a static website is simple in theory, but hosting it in a way that is fast, secure, and globally accessible requires the right architecture. AWS provides two services that work extremely well together for this purpose: Amazon S3 for storage and Amazon CloudFront for global delivery.

In this blog, I want to walk you through why S3 + CloudFront is such a powerful combination and how I implemented the entire solution in Terraform.

Why Use S3 and CloudFront Together?

1. S3 as the Website Storage

Amazon S3 is a great place to store static content like HTML, CSS, JavaScript, and images. It's reliable, scalable, and inexpensive. However, S3 alone is not optimized for global content delivery. If your bucket is in Mumbai and a user in Europe tries to access your website, the latency will be noticeable.

2. CloudFront for Global Performance

CloudFront solves this problem by delivering your content through a worldwide network of edge locations. When a user accesses your site, CloudFront serves the content from the nearest location. This reduces delays, speeds up page load time, and improves the overall user experience, no matter where the user is located.

3. Security Through OAC

Instead of making your S3 bucket public, CloudFront can access the bucket on behalf of the users through an Origin Access Control (OAC). This ensures that your S3 bucket remains private while still allowing your website to be accessible globally. It’s a secure and modern approach.

4. HTTPS Support

CloudFront also enables HTTPS by default using an AWS-managed certificate. Even if you don’t have a custom domain, CloudFront provides a secure .cloudfront.net URL for your website.

Terraform Implementation

Below is the exact Terraform code I used to deploy the entire setup. It covers S3 bucket creation, file upload, OAC configuration, bucket policies, and CloudFront distribution.

// Create an S3 bucket with a dynamic name using prefix and name variables
resource "aws_s3_bucket" "website" {
  bucket = "${var.bucket_prefix}${var.bucket_name}"
}

// Block public access to the S3 bucket
resource "aws_s3_bucket_public_access_block" "website_public_access_block" {
  bucket = aws_s3_bucket.website.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

// Create a CloudFront Origin Access Control for the S3 bucket
resource "aws_cloudfront_origin_access_control" "oac" {
  name                              = "${var.bucket_prefix}-oac"
  description                       = "OAC for ${var.bucket_name}"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

// S3 Bucket Policy to allow access from cloudfront origin access control
resource "aws_s3_bucket_policy" "website_bucket_policy" {
  bucket = aws_s3_bucket.website.id
  depends_on = [aws_s3_bucket_public_access_block.website_public_access_block]
  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "AllowCloudFrontAccess",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "cloudfront.amazonaws.com"
        },
        "Action" : [
          "s3:GetObject"
        ],
        "Resource" : "${aws_s3_bucket.website.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.s3_distribution.arn
          }
        }
      }
    ]
  })
}

// upload files to the S3 bucket
resource "aws_s3_object" "website_files" {
  for_each = fileset("${path.module}/www", "**/*")
  bucket   = aws_s3_bucket.website.id
  key      = each.value
  source   = "${path.module}/www/${each.value}"
  etag     = filemd5("${path.module}/www/${each.value}")
  content_type = lookup({
    "html" = "text/html",
    "css"  = "text/css",
    "js"   = "application/javascript",
    "json" = "application/json",
    "png"  = "image/png",
    "jpg"  = "image/jpeg",
    "jpeg" = "image/jpeg",
    "gif"  = "image/gif",
    "svg"  = "image/svg+xml",
    "ico"  = "image/x-icon",
    "txt"  = "text/plain"
  }, split(".", each.value)[length(split(".", each.value)) - 1], "application/octet-stream")
}

// Create a CloudFront Distribution to serve content from the S3 bucket
resource "aws_cloudfront_distribution" "s3_distribution" {
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id                = local.origin_id
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.origin_id

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_100"
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

Final Output

After running the usual Terraform commands—init, validate, plan, and apply—you get a CloudFront URL that securely serves your static website. The S3 bucket stays completely private while the content is delivered quickly and securely across the world.
This setup gives you a production-ready hosting environment that is fast, secure, and automated.

Reference Video


@piyushsachdeva

I Built a Chrome Extension That Auto-Replies to Tweets Using Gemini AI

2025-12-10 23:21:59

Wanted to share a side project I’ve been building.

It’s a Chrome extension that:

  • Reads your X/Twitter timeline
  • Uses the Gemini API to analyze each post
  • Generates replies automatically
  • Posts them on your behalf
  • Repeats this for 15 posts

Why?

To automate engagement and help creators stay consistent.

The tool uses:

  1. Manifest V3
  2. Content scripts controlling DOM + input
  3. Gemini 1.0 API
  4. A custom system prompt for tone control

Upcoming features:

  • Scheduled posting
  • Auto-posting tech news from free APIs
  • Smarter rate limiting + safer automation
  • Video demo attached. Would love thoughts from the dev community!


Demo video