Status: PR-Ready Concept
Author: Integration Design Task
Date: 2026-02-06
Connect is a first-party integrations platform for the Rust NYC Talk Submission System. It provides a unified, secure interface for external tools to access talk data, receive real-time notifications, and export schedulesβenabling organizers to build custom workflows without modifying the core system.
| Field | Description |
|---|---|
key_id | Public identifier (prefix: rnc_) |
key_hash | bcrypt hash of secret portion |
name | Human-readable label |
scopes | Permission array: ["submissions:read", "webhooks:manage", "exports:create"] |
created_by | Discord user ID of creator |
expires_at | Optional expiration timestamp |
last_used_at | Audit timestamp |
Key format: rnc_live_<24-char-random> (production) / rnc_test_<24-char-random> (test mode)
Schema addition (backend/index.ts, new table):
CREATE TABLE IF NOT EXISTS api_keys_1 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_id TEXT UNIQUE NOT NULL,
key_hash TEXT NOT NULL,
name TEXT NOT NULL,
scopes TEXT NOT NULL, -- JSON array
created_by TEXT NOT NULL,
expires_at DATETIME,
last_used_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
| Field | Description |
|---|---|
id | Auto-increment ID |
url | HTTPS endpoint (must be TLS) |
events | Array: ["submission.created", "submission.updated", "talk.scheduled"] |
secret | HMAC-SHA256 signing secret |
api_key_id | Owning API key |
active | Boolean toggle |
failure_count | Consecutive failures (disable at 10) |
Webhook payload structure:
{ "event": "submission.created", "timestamp": "2026-02-06T13:57:40Z", "data": { /* TalkSubmission object */ }, "signature": "sha256=..." }
Schema addition:
CREATE TABLE IF NOT EXISTS webhooks_1 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
events TEXT NOT NULL, -- JSON array
secret TEXT NOT NULL,
api_key_id INTEGER REFERENCES api_keys_1(id),
active BOOLEAN DEFAULT true,
failure_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
All endpoints require Authorization: Bearer rnc_live_... header.
| Endpoint | Scope | Description |
|---|---|---|
GET /api/v1/submissions | submissions:read | List all submissions with pagination |
GET /api/v1/submissions/:id | submissions:read | Get single submission |
GET /api/v1/schedule | submissions:read | Get scheduled talks with dates |
| Endpoint | Scope | Format |
|---|---|---|
GET /api/v1/exports/submissions.csv | exports:create | CSV download |
GET /api/v1/exports/submissions.json | exports:create | JSON array |
GET /api/v1/exports/schedule.ics | exports:create | iCalendar feed |
CSV columns: id,speaker_name,talk_context,is_on_behalf,submitter_name,discord_channel_id,created_at,scheduled_date
| Endpoint | Scope | Description |
|---|---|---|
POST /api/v1/webhooks | webhooks:manage | Register new webhook |
GET /api/v1/webhooks | webhooks:manage | List webhooks |
DELETE /api/v1/webhooks/:id | webhooks:manage | Remove webhook |
POST /api/v1/webhooks/:id/test | webhooks:manage | Send test event |
// Zapier/Make webhook handler
app.post("/api/webhooks/notion-sync", async (c) => {
const payload = await c.req.json();
if (payload.event === "submission.created") {
await notionClient.pages.create({
parent: { database_id: NOTION_DB_ID },
properties: {
"Speaker": { title: [{ text: { content: payload.data.speaker_name }}] },
"Context": { rich_text: [{ text: { content: payload.data.talk_context }}] },
"Discord": { url: payload.data.discord_invite_link },
"Status": { select: { name: "New" } }
}
});
}
});
# Cron job or manual trigger curl -H "Authorization: Bearer rnc_live_..." \ "https://rustnyc-talks.val.run/api/v1/exports/submissions.csv" \ | google-sheets-append --spreadsheet-id=... --sheet="Submissions"
<!-- Subscribe to iCal feed --> <a href="webcal://rustnyc-talks.val.run/api/v1/exports/schedule.ics?key=rnc_live_..."> Add to Calendar </a>
// Webhook endpoint receives events
async function handleWebhook(event: WebhookPayload) {
if (event.event === "talk.scheduled") {
await slack.chat.postMessage({
channel: "#rust-nyc-talks",
text: `π
"${event.data.speaker_name}" scheduled for ${event.data.scheduled_date}!`,
unfurl_links: false
});
}
}
- API Key validation: Extract from
Authorization: Bearerheader - Hash verification: Compare bcrypt hash against stored
key_hash - Expiration check: Reject if
expires_at < NOW() - Scope enforcement: Check required scope for endpoint
- TLS only: Reject non-HTTPS URLs at registration
- HMAC signing:
X-Signature: sha256=HMAC(secret, rawBody) - Timestamp header:
X-Timestampfor replay protection (5-min window) - Auto-disable: Deactivate webhook after 10 consecutive failures
| Scope | Limit |
|---|---|
| Per API key | 100 req/min |
| Webhook deliveries | 10 req/sec per endpoint |
| Export endpoints | 10 req/hour |
CREATE TABLE IF NOT EXISTS api_audit_log_1 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_key_id INTEGER,
endpoint TEXT NOT NULL,
method TEXT NOT NULL,
status_code INTEGER,
ip_address TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
| Metric | Target (90 days) | Measurement |
|---|---|---|
| API keys created | 10+ | COUNT in api_keys_1 |
| Webhooks registered | 5+ | COUNT in webhooks_1 |
| Export downloads/week | 20+ | Audit log COUNT |
| Webhook delivery success rate | >99% | Failures / Total |
| P95 API latency | <200ms | Timing middleware |
| Integration partners mentioned | 2+ | Manual tracking |
- API key table + CRUD endpoints
- Auth middleware (
backend/middleware/auth.ts) - Basic
GET /api/v1/submissionswith API key auth
- CSV export endpoint
- JSON export endpoint
- iCalendar feed (requires
scheduled_datecolumn)
- Webhook registration endpoints
- Event dispatch on submission create (
backend/index.ts:70-90) - Retry logic with exponential backoff
- Signature verification guide
- Rate limiting middleware
- Audit logging
- Developer documentation page
- API key management UI (organizer-only)
File: shared/types.ts (add after line 24)
// --- Integrations Platform Types ---
export type ApiKeyScope = "submissions:read" | "webhooks:manage" | "exports:create";
export interface ApiKey {
id: number;
key_id: string; // e.g., "rnc_live_abc123..."
name: string;
scopes: ApiKeyScope[];
created_by: string;
expires_at?: string;
last_used_at?: string;
created_at: string;
}
export type WebhookEvent = "submission.created" | "submission.updated" | "talk.scheduled";
export interface Webhook {
id: number;
url: string;
events: WebhookEvent[];
api_key_id: number;
active: boolean;
failure_count: number;
created_at: string;
}
export interface WebhookPayload<T = TalkSubmission> {
event: WebhookEvent;
timestamp: string;
data: T;
signature: string;
}
export interface ExportOptions {
format: "csv" | "json" | "ics";
since?: string; // ISO date filter
status?: "pending" | "scheduled" | "all";
}
Rust NYC Launches "Connect" β Open API for Community Tool Integrations
Organizers can now sync talks to Notion, export to Sheets, and receive real-time Slack notifications
- Rust NYC announces Connect, an API platform enabling organizers and community members to integrate talk submissions with their existing tools
- Available today with CSV/JSON exports, iCalendar feeds, and webhooks
- π Secure API keys with granular scopes
- π€ One-click CSV/JSON exports for spreadsheets
- π Subscribe to iCal feed in Google/Apple Calendar
- π Webhooks for real-time notifications to Slack, Discord, custom bots
- π HMAC-signed payloads, TLS-only, automatic rate limiting
"We used to manually copy submissions into Notion every week. Now they appear automatically within seconds."
- API documentation:
https://rustnyc-talks.val.run/docs/api - API keys: Request via Discord #organizers channel
- No cost for community use
Join Rust NYC Discord to request an API key and start building integrations today.
- Schema migration: Add
scheduled_datecolumn totalk_submissions_3(required for calendar feed) - Organizer auth: Decide how organizers authenticate to create API keys (Discord OAuth or shared admin key)
- Documentation site: Static
/docs/apipage with OpenAPI spec or Markdown - Webhook queue: Consider async job queue for reliability (Val Town cron for retries?)
- Key rotation UI: Endpoint to revoke and regenerate keys without data loss