agentsclimarketplace

Discord as backend

Skill phucbm/skills/skills/backend/discord-as-backend

Use Discord as a lightweight backend for web apps — read a Forum channel as a structured database (bookmarks, link store) or push form submissions into a Discord channel via webhook. Trigger when the user wants zero-infra persistence using Discord, or wants a no-database form submission inbox.From its SKILL.md

Install
npx -y skills add phucbm/skills --skill discord-as-backend

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

6.6 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

Discord as Web Backend — Patterns

Two proven patterns for using Discord as zero-infra backend from a Next.js app.


Pattern 1 — Forum Channel as Database

Use case: bookmarks, link store, content collection — each Discord Forum thread = one record.

How it works:

  • Create a Forum channel in your Discord server
  • Add a Bot to the server, give it Read Messages/View Channels permission
  • Each thread = one record; thread name = title; applied_tags = categories; first message = body/content

Env vars:

DISCORD_BOT_TOKEN=
DISCORD_CHANNEL_ID=   # Forum channel ID

Fetch flow (Discord REST API v10):

  1. GET /channels/{channel_id} — fetch channel metadata + available_tags (tag ID → name map)
  2. GET /channels/{channel_id}/threads/archived/public?limit=100 — archived threads
  3. GET /guilds/{guild_id}/threads/active — active threads (filter by parent_id === channel_id)
  4. For each thread: GET /channels/{thread_id}/messages?limit=1 — first message = record content

Next.js lib pattern (used in phucbm-web bookmarks):

export async function getDiscordBookmarks(): Promise<Bookmark[]> {
    const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
    const DISCORD_CHANNEL_ID = process.env.DISCORD_CHANNEL_ID;
    if (!DISCORD_BOT_TOKEN || !DISCORD_CHANNEL_ID) return [];

    const headers = { Authorization: `Bot ${DISCORD_BOT_TOKEN}` };
    const cache = { next: { revalidate: 3600 } };

    // 1. Channel metadata + tag map
    const channelData = await fetch(
        `https://discord.com/api/v10/channels/${DISCORD_CHANNEL_ID}`,
        { headers, ...cache }
    ).then(r => r.json());

    const tagMap = new Map<string, string>();
    channelData.available_tags?.forEach((t: any) => tagMap.set(t.id, t.name));

    // 2. Archived + active threads
    const [archivedData, guildData] = await Promise.all([
        fetch(`https://discord.com/api/v10/channels/${DISCORD_CHANNEL_ID}/threads/archived/public?limit=100`, { headers, ...cache }).then(r => r.json()),
        fetch(`https://discord.com/api/v10/guilds/${channelData.guild_id}/threads/active`, { headers, ...cache }).then(r => r.json()),
    ]);

    const threads = [
        ...(archivedData.threads ?? []),
        ...(guildData.threads ?? []).filter((t: any) => t.parent_id === DISCORD_CHANNEL_ID),
    ];

    // 3. First message per thread = record
    const records = await Promise.all(
        threads.map(async (thread: any) => {
            const msgs = await fetch(
                `https://discord.com/api/v10/channels/${thread.id}/messages?limit=1`,
                { headers, ...cache }
            ).then(r => r.json());
            const msg = msgs[0];
            if (!msg) return null;
            return {
                id: thread.id,
                threadName: thread.name,
                content: msg.content,
                tags: (thread.applied_tags ?? []).map((id: string) => tagMap.get(id)).filter(Boolean),
                timestamp: new Date(msg.timestamp),
                author: msg.author.username,
                url: msg.content.match(/(https?:\/\/[^\s]+)/)?.[0] ?? null,
                thumbnail: msg.embeds?.[0]?.thumbnail?.url ?? msg.attachments?.[0]?.url ?? null,
            };
        })
    );

    return records.filter(Boolean).sort((a, b) => b.timestamp - a.timestamp);
}

Caching: use next: { revalidate: 3600 } (ISR) — avoid fetching per-request, Discord rate-limits at 50 req/s.

Limitations:

  • 100 archived threads per fetch (no cursor pagination in this pattern — use multiple pages if needed)
  • Read-only from the web side; writes go through the Discord client manually
  • Not suitable for high-write or relational data

Pattern 2 — Webhook as Form Submission Inbox

Use case: contact forms, bug reports, feedback — submissions land in a Discord channel as messages.

How it works:

  • Create a Webhook in any Discord channel (Channel Settings → Integrations → Webhooks)
  • Copy the webhook URL — store it as an env var
  • POST { content: "..." } to the webhook URL — no bot, no auth header needed

Env vars:

DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy

Next.js API route (app/api/report/route.ts):

import { NextRequest, NextResponse } from "next/server";

const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL ?? "";

export async function POST(req: NextRequest) {
    if (!WEBHOOK_URL) return NextResponse.json({ error: "Not configured" }, { status: 503 });

    const { content } = await req.json();
    if (!content || typeof content !== "string")
        return NextResponse.json({ error: "Invalid body" }, { status: 400 });

    const res = await fetch(WEBHOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ content }),
    });

    if (!res.ok) return NextResponse.json({ error: "Upstream error" }, { status: 502 });
    return NextResponse.json({ ok: true });
}

Client-side call (used in hieu-chu-han ReportIssueDialog):

const footer = [url ? `<${url}>` : null, `\`v${version}\``].filter(Boolean).join("  ");
await fetch("/api/report", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: `${message}\n\n${footer}` }),
});

Discord message formatting tips:

  • Wrap URLs in <url> to suppress embeds: <https://example.com>
  • Use `backticks` for inline code, triple backtick blocks for structured data
  • **bold**, > blockquote for emphasis

Limitations:

  • No rate limiting built in — add one if exposed publicly
  • No auth on the API route — acceptable for internal/low-traffic tools; add a shared secret header for public apps
  • Webhook URL is a secret — never expose it client-side; always proxy through your own API route

Choosing between the two

Forum as DBWebhook Inbox
Auth neededBot tokenNone (webhook URL)
DirectionRead (from Discord)Write (to Discord)
Setup effortModerate (bot, permissions)Minimal (one webhook URL)
Best forBookmarks, curated contentForms, reports, notifications

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most data backend skills give in ~1.5k tokens

Counted across 258 of the 277 authors here whose files we hold, read 2026-09-06

  • Centralize error handling in one handlerin 38 of 258, across 20 files
  • Select only needed columns in queriesin 36 of 258, across 18 files
  • Check cache before querying the databasein 36 of 258, across 19 files
  • Wrap multi-step writes in transactionsin 36 of 258, across 18 files
  • Retry failed requests with exponential backoffin 36 of 258, across 18 files
  • Batch fetch related records to avoid N+1 queriesin 34 of 258, across 16 files
  • Use resource-based URLs for REST endpointsin 31 of 258, across 13 files
  • Separate business logic into a service layerin 29 of 258, across 17 files
  • Queue background jobs instead of blocking requestsin 29 of 258, across 14 files
  • Abstract data access behind repository interfacesin 25 of 258, across 13 files
  • Log structured JSON with request contextin 15 of 258, across 7 files
  • Use a shared store for rate limitingin 14 of 258, across 6 files

Said here and by no other author read

  • Store Discord credentials in environment variables
  • Treat each forum thread as one record
  • Fetch archived and active threads
  • Fetch each thread's first message as content
  • Fetch channel metadata for the tag map
  • Filter active threads by parent channel ID

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.