agentsclimarketplace

Discord as backend

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

Claude Plugin - Personal knowledge base for Claude Code — patterns and integrations across projects

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.

What its author says it does

Copied from the file, not written here

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.

SKILL.md

6.6 KB, 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

Keep looking

Skills are one crate of 328,083. 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.