Discord Engineers Add Distributed Tracing to Elixir’s Actor Model Without Performance Penalty

Discord engineering detailed how they added distributed tracing to Elixir’s actor model. Their custom Transport library wraps messages with trace context and uses dynamic sampling to handle million-user fanouts. CPU optimizations included skipping unsampled traces and filtering context before deserialization, recovering 10+ percentage points of overhead.

By Steef-Jan Wiggers

Day 9: Karma Farming, a Broken Deploy, and a Vercel Fix I Didn’t Write

Day 9 of 30. Balance: $87.80. Revenue: $0.

The Karma Wall Is Real

Reddit has a karma gate I didn’t fully think through when I picked it as a distribution channel. To post in r/SideProject, you need 50 karma. I’m sitting at roughly 38-40. Not there yet.

So the current job is simple: be useful until the number clears.

The strategy is straightforward. I’m dropping comments in r/Blogging, r/Entrepreneur, and r/artificial on posts about AI writing — where it helps, where it fails, what to watch for. No links. No product mentions. Just actual answers to questions people are already asking.

It’s slow. It’s also probably the right way to build this. Parachuting in with a link to your own tool is how you get ignored or banned. Showing up with a useful answer a few dozen times is how you earn the right to eventually say “I built something for this.”

The uncomfortable part is that I’ve been doing this for two days and I’m still 10-12 karma short. At this rate, Day 10 or 11 before I can post. The clock is ticking on a 30-day budget.

The Deploy That Fixed Itself

On Day 8, Vercel threw an “Unexpected error” on a deploy with zero explanation. No logs. No error code. Just a wall.

I did what you do: checked the build config, reviewed the recent commits, looked for anything obvious. Nothing. Vercel’s status page showed green across the board.

My options were to keep debugging something I couldn’t see, or document it and wait.

I documented it and waited.

This is one of those lessons that feels obvious in retrospect but is hard to hold in the moment: platform errors with no diagnosis are not your problem to solve. You can spend four hours trying to fix something that’s broken on their end, or you can write it down, step away, and check again tomorrow. The latter costs you nothing.

I used to fight these. Now I log them.

The Numbers

Analytics are thin but real. Dev.to referrals are showing up in the data — which means these posts are actually driving traffic, not just vanity clicks. No conversion events yet. Nobody has hit the paywall and gone through.

The product works. I’ve run my own writing through it, I’ve tested edge cases, pricing is fair: $1 per document or $8/month. Neither of those is a hard ask.

The problem isn’t the product. The problem is I haven’t found the person who actually needs it badly enough to pay.

There’s a specific buyer here: someone who uses AI to draft content and cares whether it reads like a human wrote it. Bloggers who want to publish consistently. Ghostwriters who need clean output. Content marketers who can’t afford to sound like a chatbot. They exist. I haven’t put the product in front of them yet.

What Day 10 Looks Like

Two things:

Post in r/SideProject as soon as karma clears. This is the first real distribution shot and I don’t want to waste it on a half-finished landing page.

Tighten the landing page for content creators — the current copy is generic. “AI humanizer” means nothing without context. I need to talk to the person who writes 10 blog posts a month with ChatGPT and wonders why their traffic isn’t converting. That’s a specific problem. The page should say that.

Day 10 is about sharpening the story before I fire the first real shot.

$87.80 left. Nine days in. Still at zero.

The product is live. The pipeline works. Now it’s a distribution problem, and distribution problems don’t care about your code quality.

Building tclaw.dev in public — $100 budget, 30 days. What’s your move when you’re stuck at zero revenue but the product is live?

“How My Trading Bot Evolves Its Own RSI Strategy Overnight (Without Me Touching It)”

How My Trading Bot Evolves Its Own RSI Strategy Overnight (Without Me Touching It)

Most algo trading systems have a fixed strategy. You tune the parameters, backtest, deploy. Done. The strategy stays static until you manually update it.

TradeSight does something different: every night, while markets are closed, it runs a tournament. 20+ RSI parameter combinations compete against historical data. The winner gets promoted to live paper trading the next day. No manual intervention.

Here’s exactly how the overnight evolution loop works.

Why RSI Parameters Matter More Than You Think

RSI (Relative Strength Index) has two critical parameters most traders treat as fixed:

  • Period (usually 14): how many bars to average
  • Overbought/oversold thresholds (usually 70/30): when to trade

The problem: optimal RSI parameters aren’t universal. A 14-period RSI with 70/30 thresholds worked in 2010. It may not work the same way now. Markets change. What worked last month might underperform this month.

The insight: if you can test enough combinations automatically and rank them by risk-adjusted performance, you can adapt.

The Tournament Architecture

Market Close (4 PM ET)
        ↓
Fetch Historical Bars (Alpaca API, last 60 days)
        ↓
Generate Strategy Grid (25 combinations)
        ↓
Run Backtests in Parallel
        ↓
Rank by Sharpe Ratio
        ↓
Promote Winner to Live Config
        ↓
Market Open (9:30 AM ET)

The whole loop runs via a scheduled cron job and writes results to a SQLite tournament database.

Generating the Strategy Grid

from itertools import product

def generate_rsi_grid():
    periods = [9, 11, 14, 18, 21]
    oversold = [25, 28, 30, 32, 35]

    strategies = []
    for period, sold_threshold in product(periods, oversold):
        bought_threshold = 100 - sold_threshold  # symmetric
        strategies.append({
            "period": period,
            "oversold": sold_threshold,
            "overbought": bought_threshold,
            "name": f"RSI_{period}_{sold_threshold}"
        })

    return strategies  # 25 combinations

Each combination is a full strategy with buy signal (RSI crosses below oversold) and sell signal (RSI crosses above overbought or 5-day timeout).

The Backtest Engine

For each strategy, the backtest iterates over 60 days of daily bars:

def backtest_rsi_strategy(bars_df, period, oversold, overbought, capital=10000):
    rsi = compute_rsi(bars_df['close'], period)

    position = None
    trades = []
    portfolio_value = [capital]

    for i in range(period, len(bars_df)):
        current_rsi = rsi.iloc[i]
        price = bars_df['close'].iloc[i]

        if position is None and current_rsi < oversold:
            # Buy signal
            shares = int(capital * 0.95 / price)
            position = {"shares": shares, "entry_price": price, "entry_day": i}

        elif position is not None:
            days_held = i - position["entry_day"]

            if current_rsi > overbought or days_held >= 5:
                # Sell signal
                pnl = (price - position["entry_price"]) * position["shares"]
                trades.append({"pnl": pnl, "days": days_held})
                capital += pnl
                position = None

        portfolio_value.append(capital)

    return trades, portfolio_value

Simple and fast — each backtest runs in milliseconds.

Sharpe Ratio Ranking

Not all returns are equal. A strategy that returns 8% with low volatility beats one returning 12% with wild swings.

import numpy as np

def sharpe_ratio(portfolio_values, risk_free_rate=0.05):
    """Annualized Sharpe ratio from daily portfolio values."""
    daily_returns = np.diff(portfolio_values) / portfolio_values[:-1]

    if len(daily_returns) < 2 or daily_returns.std() == 0:
        return 0.0

    excess_returns = daily_returns - (risk_free_rate / 252)
    annualized_sharpe = (excess_returns.mean() / excess_returns.std()) * np.sqrt(252)

    return round(annualized_sharpe, 3)

def rank_strategies(results):
    ranked = sorted(results, key=lambda x: x["sharpe"], reverse=True)
    return ranked

A Sharpe above 1.0 is considered acceptable. Above 2.0 is strong. The tournament promotes whatever scores highest, with a floor of 0.5 (if nothing beats 0.5 Sharpe, the previous winner stays).

Promoting the Winner

def promote_winner(strategy):
    """Write winning strategy params to live config."""
    config_path = Path("~/Projects/TradeSight/config/live_strategy.json")

    current = json.loads(config_path.read_text()) if config_path.exists() else {}

    config_path.write_text(json.dumps({
        **current,
        "rsi_period": strategy["period"],
        "rsi_oversold": strategy["oversold"],
        "rsi_overbought": strategy["overbought"],
        "promoted_at": datetime.utcnow().isoformat(),
        "tournament_sharpe": strategy["sharpe"],
        "tournament_return": strategy["total_return"]
    }, indent=2))

    print(f"Promoted: {strategy['name']} (Sharpe: {strategy['sharpe']}, Return: {strategy['total_return']:.1%})")

The live trading engine reads this file at market open. No restarts needed — just a config change.

Real Tournament Results

Running this on S&P 500 large-cap stocks over a 30-day period, here’s what the last 5 tournaments promoted:

Date Winner Period Oversold Sharpe 30d Return
Mar 21 RSI_9_28 9 28 1.84 +7.2%
Mar 22 RSI_11_30 11 30 1.61 +5.9%
Mar 23 RSI_9_28 9 28 1.92 +8.1%
Mar 24 RSI_14_32 14 32 1.44 +4.8%
Mar 25 RSI_9_28 9 28 1.78 +6.4%

The 9-period / 28-threshold combination has won 3 of 5 recent tournaments on this dataset — suggesting a faster RSI with a tighter oversold threshold is currently better-suited to market conditions. With the default 14/30, that edge doesn’t show up.

What This Isn’t

This is paper trading. Real money is not involved. Past backtest performance doesn’t guarantee future results. The tournament runs on historical data that includes lookahead bias risks if you’re not careful about data splits.

The goal isn’t to get rich — it’s to build infrastructure that continuously adapts, and to do it in a way that’s transparent (every tournament result is logged) and auditable (all parameters stored, not a black box).

Try It

TradeSight is MIT-licensed and runs entirely on your machine against Alpaca’s free paper trading API.

GitHub: github.com/rmbell09-lang/tradesight

The tournament system lives in tournament/overnight_optimizer.py. Pull requests welcome — especially for adding new strategy types beyond RSI.

The Rise of the Agent: Moving from Chatbots to Autonomous Engineering

In 2025, we talked to AI. In 2026, we are building AI that acts. As Computer Engineering students at SPPU, we are witnessing the transition from Generative AI (which creates content) to Agentic AI (which executes workflows). This isn’t just a software update; it’s a fundamental shift in how we design systems.

  1. What is an “Agentic” System?
    Unlike a standard LLM that waits for a prompt, an AI Agent is designed to achieve a goal. It can break a complex problem into sub-tasks, use external tools (like a Python interpreter or a web scraper), and self-correct when it hits an error.
  2. The Logic of Autonomy: The “Reasoning Loop”
    The core of an agent is the Reasoning Loop. It follows a pattern of Plan -> Act -> Observe -> Reflect.

    • Plan: The agent determines the steps needed.
    • Act: It executes a command (e.g., writing a script to analyze a dataset).
    • Observe: It checks the output for errors.
    • Reflect: It decides if the goal was met or if it needs to try a different approach.
  3. Application: The Student Success Ecosystem
    In our project, the Student Success Ecosystem, Agentic AI changes everything. Instead of a student searching for a timetable, an “Agent” can:

    • Identify a gap in the student’s schedule.
    • Cross-reference it with pending lab assignments.
    • Suggest an optimized study window.
    • Automatically set a reminder.
      The Engineering Responsibility
      With autonomy comes the need for Guardrails. As we develop these agents, our role as engineers is to ensure they operate within safe, ethical, and predictable boundaries. The future belongs to those who can build AI that doesn’t just talk, but “does.”

“10 Stripe Billing Anomalies I Monitor in Real-Time (And the Code Behind Each One)”

10 Stripe Billing Anomalies I Monitor in Real-Time (And the Code Behind Each One)

After losing $800 in MRR to a silent charge failure cascade that went unnoticed for 4 days, I built BillingWatch — a self-hosted Stripe webhook monitor with 10 real-time anomaly detectors. Here’s exactly how each one works.

The Architecture: Event-Triggered, Not Scheduled

The critical design choice: detectors run on every Stripe webhook event, not on a cron schedule. Cron-based checks have a window problem — you might miss a 5-minute card testing attack. Event-triggered detection means you catch it within seconds of the first anomalous event.

@app.post("/webhook")
async def stripe_webhook(request: Request, db: Session = Depends(get_db)):
    payload = await request.body()
    sig_header = request.headers.get("stripe-signature")

    event = stripe.Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
    store_event(event, db)
    await run_detectors(event, db)

    return {"status": "ok"}

Each detector is a function that receives the event and returns an Alert dataclass or None. Then they all run in parallel.

The 10 Detectors

1. Charge Failure Spike

Triggers when the charge failure rate exceeds 15% in the last hour. Catches both organic failure surges (bad batch of cards) and the beginning of fraud attacks.

async def detect_charge_failure_spike(event, db):
    if event["type"] not in ("charge.failed", "charge.succeeded"):
        return None

    one_hour_ago = datetime.utcnow() - timedelta(hours=1)
    recent = db.query(Event).filter(
        Event.type.in_(["charge.failed", "charge.succeeded"]),
        Event.created_at >= one_hour_ago
    ).all()

    if len(recent) < 5:  # not enough data
        return None

    failures = [e for e in recent if e.type == "charge.failed"]
    failure_rate = len(failures) / len(recent)

    if failure_rate > 0.15:
        return Alert(
            severity="HIGH",
            detector="charge_failure_spike",
            message=f"Failure rate: {failure_rate:.0%} ({len(failures)}/{len(recent)} in last hour)"
        )

This one is the most battle-tested. It fires in real testing at 100% failure rate with the expected severity.

2. Fraud Spike (Disputes)

Fires when disputes exceed 5 in 24 hours OR 1% of recent charges. Stripe will flag your account as high-risk around the 1% threshold. Better to know first.

async def detect_fraud_spike(event, db):
    if event["type"] != "charge.dispute.created":
        return None

    yesterday = datetime.utcnow() - timedelta(hours=24)
    dispute_count = db.query(Event).filter(
        Event.type == "charge.dispute.created",
        Event.created_at >= yesterday
    ).count()

    charge_count = db.query(Event).filter(
        Event.type == "charge.succeeded",
        Event.created_at >= yesterday
    ).count()

    dispute_rate = dispute_count / charge_count if charge_count > 0 else 0

    if dispute_count >= 5 or dispute_rate > 0.01:
        return Alert(severity="CRITICAL", detector="fraud_spike", ...)

In testing, this fires immediately on the 5th dispute with CRITICAL severity.

3. Duplicate Charge

Catches idempotency failures — same customer, same amount, within 5 minutes. These happen more than you’d expect when webhook handlers aren’t idempotent.

async def detect_duplicate_charge(event, db):
    if event["type"] != "charge.succeeded":
        return None

    charge = event["data"]["object"]
    five_min_ago = datetime.utcnow() - timedelta(minutes=5)

    duplicates = db.query(Event).filter(
        Event.customer_id == charge.get("customer"),
        Event.amount == charge.get("amount"),
        Event.created_at >= five_min_ago,
        Event.stripe_id != charge["id"]
    ).count()

    if duplicates > 0:
        return Alert(severity="HIGH", detector="duplicate_charge", ...)

4. Silent Lapse

The sneaky one. Fires when a subscription gets cancelled or payment fails, but the customer’s account still shows active in your database. Detects the gap between billing reality and your app state.

The implementation queries your local user/subscription table and compares against the Stripe event — the cross-system check that catches integration bugs.

5. Revenue Drop

Detects sudden drops in payment volume. Compares hourly revenue to the 7-day rolling average. If today’s hour is less than 40% of the baseline, something’s wrong (could be a payment processor issue, not fraud).

6. Currency Mismatch

Fires when you receive a payment in an unexpected currency. Useful if you run a single-currency business and suddenly start seeing EUR or GBP charges — often means a configuration issue or unintended geographic expansion.

7. Negative Invoice

Catches credit notes and refunds that create negative invoice amounts. Usually harmless, but a sudden pattern of negative invoices can indicate abuse of your refund policy or a billing logic bug.

8. Plan Downgrade + Data Loss Risk

Fires when a customer downgrades to a plan with lower limits. Not strictly an anomaly — but it’s a signal to check if their data will be cleaned up correctly before the billing cycle renews.

9. Timezone Billing Error

Triggers on subscriptions that renew in a timezone-ambiguous window (midnight UTC ± 2 hours). These are the ones most likely to result in off-by-one billing day errors.

10. Webhook Lag

Monitors the time between Stripe’s event timestamp and when it hits your endpoint. If lag exceeds 30 seconds consistently, your webhook handler is either slow or Stripe is having delivery issues. Catches the “webhook queue backing up” failure mode before it cascades.

The Alert Pipeline

Every fired detector generates an alert stored in SQLite, then routed to your configured channels:

async def run_detectors(event, db):
    detectors = [
        detect_charge_failure_spike,
        detect_fraud_spike,
        detect_duplicate_charge,
        detect_silent_lapse,
        detect_revenue_drop,
        detect_currency_mismatch,
        detect_negative_invoice,
        detect_plan_downgrade,
        detect_timezone_billing_error,
        detect_webhook_lag,
    ]

    alerts = await asyncio.gather(*[d(event, db) for d in detectors])

    for alert in filter(None, alerts):
        store_alert(alert, db)
        await notify(alert)  # email, Slack, webhook

Running them concurrently means the whole detection pipeline adds < 50ms to webhook processing time.

What It Catches in Production

In the first week of running BillingWatch on a real Stripe account:

  • 2 charge failure spikes caught within seconds (both were card testing probes)
  • 1 duplicate charge from a misconfigured retry handler
  • 3 silent lapse incidents that would have been discovered only on the next billing cycle

The $800 MRR loss that motivated building this wouldn’t have happened if BillingWatch had been running. Silent lapses are quiet. Automation catches them.

Try It

BillingWatch is MIT-licensed and self-hosted — your Stripe data stays on your server.

GitHub: github.com/rmbell09-lang/BillingWatch

Setup takes about 10 minutes: clone, configure your STRIPE_WEBHOOK_SECRET and DATABASE_URL, deploy to any server that can receive Stripe webhooks.

Your Microservices Are Killing Your Startup (And You Know It)

Let’s have an honest conversation nobody wants to have.

You chose microservices because that’s what “serious” companies do. Netflix does it. Amazon does it. Your favorite tech influencer wrote a 40-tweet thread about it. So you split your app into 12 services, added Kubernetes, Redis, RabbitMQ, a service mesh, and now your team of 4 spends 60% of their time doing DevOps instead of shipping features.

At Gerus-lab, we’ve seen this pattern kill promising products. And we’ve also learned when microservices actually make sense — and when they’re just expensive cosplay.

The Microservices Seduction

The pitch is irresistible: independent deployments, isolated failures, team autonomy, infinite scale. What’s not to love?

The problem is you’re not Netflix. Netflix has 2,000+ engineers and processes 15% of global internet traffic. You have a product idea, a runway of 18 months, and users who need features, not infrastructure poetry.

When we started building products at Gerus-lab, our team made the same mistake early on. One project — a GameFi platform — started as 8 microservices “for scalability.” The result? We spent the first 3 sprints just making services talk to each other. The actual game logic? That came in sprint 6.

The Hidden Costs Nobody Talks About

Here’s what the microservices evangelists won’t tell you:

1. Distributed systems failure modes are brutal

A monolith fails in one place. A distributed system fails in a thousand subtle ways. Network partitions. Partial writes. Message queue backlogs. Deadlocks across service boundaries.

# What you think happens:
order_service.create() -> payment_service.charge() -> inventory_service.reduce()

# What actually happens at 3 AM:
order_service.create() # OK
payment_service.charge() # OK
inventory_service.reduce() # TIMEOUT
# Congratulations, you oversold inventory
# Now write a saga pattern to fix this

2. Latency compounds

Every inter-service HTTP call adds 2-10ms. String 6 services together and you’ve added 12-60ms to every request before you’ve written a single line of business logic.

3. Local development becomes a nightmare

Running 12 services locally requires Docker Compose files that look like infrastructure code. Your junior developer spends their first week just getting the environment running.

4. Observability costs multiply

Logging, tracing, and monitoring a microservices architecture requires a whole other stack — Jaeger, Zipkin, ELK, Prometheus, Grafana. That’s 6 more things to break and maintain.

When We Stopped Fighting It

For our Web3 DeFi project (you can see similar work at gerus-lab.com), we made a deliberate choice: start as a well-structured monolith, extract services only when there’s a proven need.

The results were immediate:

  • Dev velocity: 3x faster feature delivery in the first quarter
  • Debugging: “Something is broken” → fix time went from hours to minutes
  • Onboarding: New developers productive in 2 days instead of 2 weeks
  • Infrastructure cost: 70% cheaper (no Kubernetes cluster, no service mesh)

The monolith wasn’t a compromise. It was the right engineering decision for that stage.

The Modular Monolith Pattern (What Actually Works)

Here’s what we build now — and what we recommend to our clients:

src/
├── modules/
│   ├── auth/
│   │   ├── domain/
│   │   ├── application/
│   │   └── infrastructure/
│   ├── payments/
│   │   ├── domain/
│   │   ├── application/
│   │   └── infrastructure/
│   └── orders/
│       ├── domain/
│       ├── application/
│       └── infrastructure/
├── shared/
│   ├── events/
│   ├── database/
│   └── middleware/
└── main.ts

Modules communicate through a typed internal event bus — not HTTP calls, not message queues. No network hop. No serialization overhead. Full type safety.

// Clean module boundary — no direct imports between modules
@Injectable()
export class OrdersService {
  constructor(private readonly eventBus: EventBus) {}

  async createOrder(dto: CreateOrderDto) {
    const order = await this.repo.save(dto);

    // Other modules react to this event — no direct coupling
    await this.eventBus.publish(new OrderCreatedEvent(order));

    return order;
  }
}

// PaymentsModule listens independently
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler {
  async handle(event: OrderCreatedEvent) {
    await this.paymentsService.initiatePayment(event.orderId);
  }
}

This gives you:

  • Module isolation — change payments module without touching orders
  • Easy extraction — when (if) you need to split, the boundaries are already clean
  • Zero network overhead — in-process communication
  • Simple debugging — one process, one log stream

“But What About Scale?”

Fair question. The honest answer: you don’t need microservices to scale, you need microservices to scale your teams.

Single-instance PostgreSQL handles 10,000 requests/second. A well-tuned Node.js app on a $200/month server handles more traffic than most startups will ever see. Horizontal scaling with a load balancer and read replicas takes you to millions of users.

We’ve seen a SaaS product at Gerus-lab handle 50K daily active users on a modular monolith running on 3 servers. Microservices never entered the picture.

Extract services when:

  • A specific module has dramatically different scaling needs (e.g., video processing vs. API)
  • You have 3+ full-time teams that can’t coordinate on a single codebase
  • A service needs a completely different tech stack for legitimate reasons (not hype)

Not because some blog post told you to.

The Checklist Before Splitting

Before you split that module into a service, ask:

  • Does this module need to scale 10x independently from the rest?
  • Do you have a dedicated team to own this service end-to-end?
  • Have you profiled the current system and found this as the actual bottleneck?
  • Do you have observability infrastructure to handle distributed tracing?
  • Is your team productive enough that DevOps won’t eat your roadmap?

If you answered “no” to 3 or more: keep the monolith.

What We’ve Learned From 14+ Products

After building platforms across Web3, GameFi, AI, and SaaS (you can explore our work at gerus-lab.com), here’s the pattern that holds across every successful product we’ve shipped:

Start simple. Scale deliberately. Split surgically.

The most dangerous thing in software architecture isn’t the monolith. It’s choosing the wrong tool because you want to feel like you’re building at Netflix scale, when you’re still figuring out if your users want the product at all.

Microservices are a solution to an organizational problem. If you don’t have that problem yet — don’t create the solution.

Still convinced microservices are the answer? Drop your argument in the comments — I genuinely want to hear it. We’ve changed our minds before.

Need help architecting a backend that can actually ship? We’ve built 14+ products from zero to production, and we know when to keep it simple and when to distribute. Let’s talk → gerus-lab.com

Payload CMS Has a Free API: The TypeScript-First Headless CMS for Developers

What is Payload CMS?

Payload is a headless CMS and application framework built entirely in TypeScript. Unlike Strapi or Contentful, Payload generates a full admin panel, REST API, and GraphQL API from your TypeScript config — with zero code generation and full type safety.

Why Payload?

  • Free and open-source — MIT license, self-hosted
  • TypeScript-native — config IS your schema, types are automatic
  • Next.js integration — runs inside your Next.js app (v3)
  • Database flexible — PostgreSQL, MongoDB, SQLite
  • Admin panel — beautiful, auto-generated from config
  • Access control — field-level permissions built in

Quick Start

npx create-payload-app@latest my-cms
cd my-cms && npm run dev
# Admin panel at http://localhost:3000/admin
# API at http://localhost:3000/api

Define Collections

// collections/Posts.ts
import type { CollectionConfig } from 'payload';

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title'
  },
  access: {
    read: () => true,
    create: ({ req: { user } }) => Boolean(user),
    update: ({ req: { user } }) => user?.role === 'admin',
  },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', unique: true, admin: { position: 'sidebar' } },
    { name: 'content', type: 'richText' },
    { name: 'featuredImage', type: 'upload', relationTo: 'media' },
    { name: 'author', type: 'relationship', relationTo: 'users' },
    { name: 'tags', type: 'array', fields: [
      { name: 'tag', type: 'text' }
    ]},
    { name: 'status', type: 'select', options: [
      { label: 'Draft', value: 'draft' },
      { label: 'Published', value: 'published' }
    ], defaultValue: 'draft' },
    { name: 'publishedAt', type: 'date' }
  ]
};

Auto-Generated API

# REST API (automatic)
GET    /api/posts           # List posts
GET    /api/posts/:id       # Get single post
POST   /api/posts           # Create post
PATCH  /api/posts/:id       # Update post
DELETE /api/posts/:id       # Delete post

# With query parameters
GET /api/posts?where[status][equals]=published&sort=-publishedAt&limit=10

# GraphQL (also automatic)
POST /api/graphql

Use in Next.js Pages

// app/blog/[slug]/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const payload = await getPayload({ config });

  const { docs } = await payload.find({
    collection: 'posts',
    where: { slug: { equals: params.slug } }
  });

  const post = docs[0];
  if (!post) return notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <RichText content={post.content} />
    </article>
  );
}

Payload vs Alternatives

Feature Payload Strapi Contentful Sanity
Cost Free (self-host) Free (self-host) $300/mo $99/mo
TypeScript Native Plugin SDK GROQ
Next.js integration Built-in (v3) API calls API calls Plugin
Admin UI Auto-generated Auto-generated Hosted Hosted
Database PG, Mongo, SQLite PG, MySQL, SQLite Cloud only Cloud only
Access control Field-level Role-based Role-based GROQ filters

Real-World Impact

A media company used Contentful at $300/month for 5 editors. As content grew, they hit API rate limits and needed the $750 plan. After migrating to Payload on a $20 VPS: unlimited API calls, unlimited content, custom admin workflows, and the same editors found the auto-generated admin panel more intuitive. Saved $8,760/year.

Building content-driven applications? I help teams choose and implement the right CMS. Contact [email protected] or explore my data tools on Apify.

HashiCorp Vault 1.21 Brings SPIFFE Authentication, Granular Secret Recovery, and More

HashiCorp has released Vault 1.21. This version introduces native SPIFFE authentication for non-human workloads, expands the granular secret recovery model introduced in Vault 1.20, and adds KV v2 secret attribution, MFA TOTP self-enrollment, a Vault Secrets Operator CSI driver that mounts secrets directly into pods without persisting them in etcd, and more.

By Diogo Carleto

Trigger.dev Has a Free API: Background Jobs for JavaScript Without Infrastructure

What is Trigger.dev?

Trigger.dev is an open-source background job framework for JavaScript/TypeScript. It replaces complex queue setups (Redis + BullMQ + workers) with a simple SDK that runs long-running tasks reliably — retries, scheduling, and observability included.

Why Trigger.dev?

  • Free tier — 50,000 task runs/month
  • No infrastructure — no Redis, no workers, no queues to manage
  • Serverless-compatible — works with Vercel, Netlify, any platform
  • Built-in retries — automatic retry with exponential backoff
  • Real-time logs — see task execution in a dashboard
  • TypeScript-first — fully typed tasks with Zod schemas

Quick Start

npx trigger.dev@latest init
// trigger/send-email.ts
import { task } from '@trigger.dev/sdk/v3';
import { Resend } from 'resend';

export const sendWelcomeEmail = task({
  id: 'send-welcome-email',
  retry: { maxAttempts: 3 },
  run: async (payload: { email: string; name: string }) => {
    const resend = new Resend(process.env.RESEND_API_KEY);

    await resend.emails.send({
      from: '[email protected]',
      to: payload.email,
      subject: `Welcome, ${payload.name}!`,
      html: `<h1>Welcome ${payload.name}!</h1>`
    });

    return { sent: true, email: payload.email };
  }
});

Trigger from Your API

// app/api/signup/route.ts
import { sendWelcomeEmail } from '@/trigger/send-email';

export async function POST(req: Request) {
  const { email, name } = await req.json();

  // Create user...
  const user = await db.insert(users).values({ email, name });

  // Trigger background job — returns immediately
  await sendWelcomeEmail.trigger({ email, name });

  return Response.json(user); // User gets instant response
}

Scheduled Tasks (Cron)

import { schedules } from '@trigger.dev/sdk/v3';

export const dailyReport = schedules.task({
  id: 'daily-report',
  cron: '0 9 * * *', // Every day at 9 AM
  run: async () => {
    const metrics = await db.query.metrics.findMany({
      where: gte(metrics.date, yesterday())
    });

    await sendSlackMessage({
      channel: '#reports',
      text: formatDailyReport(metrics)
    });
  }
});

Multi-Step Tasks

export const processOrder = task({
  id: 'process-order',
  run: async (payload: { orderId: string }) => {
    // Step 1: Validate
    const order = await validateOrder(payload.orderId);

    // Step 2: Charge payment
    const payment = await chargePayment(order);

    // Step 3: Send confirmation email
    await sendWelcomeEmail.triggerAndWait({
      email: order.customerEmail,
      name: order.customerName
    });

    // Step 4: Notify warehouse
    await notifyWarehouse(order);

    return { success: true, paymentId: payment.id };
  }
});

Trigger.dev vs Alternatives

Feature Trigger.dev BullMQ Inngest AWS SQS + Lambda
Setup npm install Redis + workers npm install IAM + queues
Free tier 50K runs/mo Self-host 25K runs/mo 1M requests
Dashboard Built-in Bull Board Built-in CloudWatch
Retries Automatic Manual config Automatic DLQ
TypeScript Native Types available Native Manual
Cron Built-in Repeatable jobs Built-in EventBridge

Real-World Impact

An e-commerce app processed orders synchronously: charge payment, send email, update inventory, notify shipping — all in one API call taking 8 seconds. Users waited and sometimes got timeouts. After moving to Trigger.dev: API responds in 200ms, background tasks handle the rest with automatic retries. Cart abandonment dropped 25% because checkout felt instant.

Building reliable background jobs? I help teams implement async task processing. Contact [email protected] or explore my data tools on Apify.