Serverless Discord interactions framework for AWS Lambda
Project description
📡 Cordless
A serverless Discord interactions framework for AWS Lambda
Cordless lets you build Discord bots without running a server — just functions, deployed to Lambda.
- No WebSockets
- No stateful runtime
- No gateway sharding
Just HTTP → functions → responses.
✨ Why Cordless?
Traditional Discord bots require:
- persistent servers
- WebSocket connections
- intent configuration
- runtime state management
Cordless flips that model:
Discord sends events → AWS Lambda runs your code → you return a response
⚡ Core Idea
Discord Interaction
│
▼
API Gateway
│
▼
AWS Lambda
│
▼
Cordless Router
│
▼
Your Functions
│
▼
JSON Response back to Discord
🚀 Quickstart
Install
pip install cordless
Create your first bot
from cordless import Cordless
bot = Cordless()
@bot.command("ping")
async def ping(ctx):
await ctx.send("pong")
Lambda entry point
import os
from cordless import Cordless
bot = Cordless(public_key=os.environ["DISCORD_PUBLIC_KEY"])
@bot.command("ping")
async def ping(ctx):
await ctx.send("pong")
def handler(event, context):
return bot.handle(event)
🔒 Request verification
Every request Discord sends to your endpoint is signed with Ed25519. Pass your
application's public key (from the Discord Developer Portal) to Cordless()
and every incoming request is verified before your handlers ever run —
requests with a missing or invalid signature are rejected with 401 and never
reach your code.
bot = Cordless(public_key=os.environ["DISCORD_PUBLIC_KEY"])
PING interactions, which Discord sends when you first configure your
endpoint URL, are answered automatically.
Omitting
public_keyskips verification — useful for local testing, but never deploy without it: anyone who finds your Lambda URL could otherwise forge interactions.
🗒️ Registering commands with Discord
@bot.command(...) only wires up local dispatch — Discord also needs to know
your commands exist so it can show them in the client. Give each command a
description (and options, if it takes arguments):
@bot.command(
"echo",
description="Repeats what you say",
options=[
{"name": "text", "description": "Text to repeat", "type": 3, "required": True},
],
)
async def echo(ctx):
await ctx.send(ctx.options["text"])
Then sync them to Discord with the cordless CLI (installed alongside the
package) — run this once after deploying, and again whenever a command's
shape changes. Point it at MODULE:ATTRIBUTE, wherever your Cordless()
instance lives:
export DISCORD_BOT_TOKEN=...
cordless register app:bot # global — every authorized guild, every user
cordless register app:bot --guild-id 123456789 # a single guild, for instant updates while developing
The application id is resolved from the bot token, so that's all you need to
provide. Omit --guild-id to register globally; global commands can take
up to an hour to propagate, so use --guild-id while iterating.
No bot token? Use client credentials instead
If your app never needs a bot user — it only ever responds to HTTP interactions, like everything cordless does — you don't need a bot token at all. Discord also accepts an OAuth2 client credentials grant for managing commands, authenticated with just your app's client ID and secret (from the Developer Portal's OAuth2 page):
export DISCORD_CLIENT_ID=...
export DISCORD_CLIENT_SECRET=...
cordless register app:bot
If both a bot token and client credentials are available, the bot token takes precedence.
Prefer calling it from code instead (e.g. inside a deploy script)? Use
bot.sync_commands(bot_token=..., guild_id=...) or
bot.sync_commands(client_id=..., client_secret=..., guild_id=...) directly
— it's what the CLI calls under the hood.
Command arguments show up on ctx.options as a plain dict, e.g. ctx.options["text"].
🧩 Commands & Interactivity
Commands
@bot.command("hello")
async def hello(ctx):
await ctx.send("Hello world!")
Buttons
Note: the
@bot.button(...)decorator below works today. Sending button components (thecomponents=argument andcordless.ui.Buttonclass) is still in active development.
Send a button:
from cordless.ui import Button
@bot.command("ping")
async def ping(ctx):
await ctx.send(
"pong",
components=[
Button(label="Edit", custom_id="edit_ping")
]
)
Handle button clicks:
@bot.button("edit_ping")
async def edit_ping(ctx):
await ctx.edit("edited")
🧠 Key concepts
Stateless by design:
- interaction payload
- custom_id routing
- Lambda invocation context
No WebSocket required.
📦 Architecture
src/cordless/
├── __init__.py
├── app.py
├── router.py
├── context.py
├── verify.py
├── register.py
├── errors.py
└── response/
└── responder.py
💡 Philosophy
Cordless is built around one idea:
Discord apps should feel like serverless functions, not servers.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cordless-0.2.1.tar.gz.
File metadata
- Download URL: cordless-0.2.1.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c69014b1a6ffaff3dc6d1ca5df0df1dbf657ea39bc65e8d529a314b70acfb35
|
|
| MD5 |
f821943c1ebba8776ebd89e46b963b08
|
|
| BLAKE2b-256 |
975b4d6ffe591b39f0b3650fcffbb7168df226ef8caf2b11aa06daa007b9d4d2
|
File details
Details for the file cordless-0.2.1-py3-none-any.whl.
File metadata
- Download URL: cordless-0.2.1-py3-none-any.whl
- Upload date:
- Size: 11.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4faa410065f33feea1d1a6b66ab49b110e122b3ee1e38829b276a17b9ebeb32
|
|
| MD5 |
bcb5f5b73c83bd6de08afdd9abf43489
|
|
| BLAKE2b-256 |
c34c49f5654366eb1c63824f99e6e9d35c93a82abf8707475d9fd99e50c2f056
|