
EdgeChat
Self-hosted team channels, direct messages and optional file sharing on Cloudflare
EdgeChat is a self-hosted Discord/Slack alternative built on Cloudflare (D1, Durable Objects, KV, R2, Workers). Paid services required. Inspect the source and license in the linked repository.
Source & license
Upstream license: GPL-3.0-or-later
License TL;DR
You can run it and change it privately. If you give others copies of the program or a modified version, provide the corresponding source under the GPL. You can charge money. Running it as a web service alone doesn’t trigger that source-sharing requirement.
Explain GPL v3 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Paid services required
Budget for Workers Paid from US$5 per account/month plus usage above included allowances. The core uses Workers, D1, KV and SQLite Durable Objects; R2 is optional for attachments. Free-plan eligibility for this build has not been established by CPU testing.
Hosting requirements
- Password hashing uses PBKDF2 with 100,000 iterations. Worker Free has a 10ms CPU ceiling; authentication, encryption, upload and garbage-collection costs need deployment measurements before claiming free hosting.
- The deployment workflow generates wrangler.ci.toml from wrangler.example.toml, supplies resource IDs and may omit R2 when the account has not enabled it. The static demo configuration is a separate deployment.
- New messages and attachments use server-side encryption, not end-to-end encryption; trust the deployer and Cloudflare, preserve all historical encryption keys, and review optional Telegram/cross-instance disclosure.
- Workers Paid has a US$5 minimum per account/month; this is a pricing floor, not a measured total. Requests, CPU, DO duration, D1 rows/storage, KV operations, R2 and optional providers need a workload budget.
- No paid plan, Cloudflare resources or upstream deployment were created by this catalog review.
Sources checked 01/10/2026
Repository snapshot: 848c95b. Hosting eligibility reflects the deployment documentation and listed assumptions.
- slack ↗
Public groups, private groups, and one-to-one direct messages.
- discord ↗
Real-time messages, paginated message history, voice messages, and file sharing.
- workers ↗
main = "worker/src/index.js"
- d1 ↗
[[d1_databases]] binding = "DB"
- kv ↗
[[kv_namespaces]] binding = "SESSIONS"
- r2 ↗
[[r2_buckets]] binding = "FILES"
- durable-objects ↗
new_sqlite_classes = ["ChannelRoom", "Scheduler"]
- paid ↗
The Workers Paid plan includes Workers, Pages Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account.
- paid ↗
| **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |
- paid ↗
iterations: 100000, hash: 'SHA-256'
- paid ↗
run_worker_first = true
- paid ↗
R2 is not enabled for this Cloudflare account; deploying without FILES binding.
- GPL-3.0-or-later ↗
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
- architecture ↗
[assets] directory = "./frontend/dist" not_found_handling = "single-page-application" binding = "ASSETS"
- architecture ↗
crons = ["*/15 * * * *"]
- architecture ↗
const tasks = [rescueBridgeDeliveries(env), rescueTelegramNotifications(env)];
- architecture ↗
return time.getUTCHours() === 19 && time.getUTCMinutes() === 0;
- architecture ↗
this.state.acceptWebSocket(server);
- architecture ↗
this.state.acceptWebSocket(server);
- architecture ↗
This project is licensed under **GNU GPL v3.0 or later**. See [LICENSE](LICENSE).
- architecture ↗
This is server-side encryption, not end-to-end encryption.
Upstream screenshot · aozorae/Edgechat repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Self-managed team groups, direct messages, history and shared files. Enterprise administration, SSO, Slack integrations and migration compatibility have not been established.
See supporting source ↗Small-community text groups, direct messages and recorded voice/file sharing. No claim of live voice/video rooms, large-scale moderation or complete Discord parity.
See supporting source ↗How it works
The shape of EdgeChat on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram based on the linked repository documentation. See the sources and hosting assumptions above.
View upstream source ↗Configuration and workflow sources
Reviewed commit 848c95b32ae2. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-04-05
edgechat-demo · default
Static assets: ./frontend/demo-dist · single-page-application
Static assets→ Static assets
Cloudflare Workers · template copied into a deployment workflow; generated values need setup · compatibility 2026-04-05
.github/workflows/deploy-worker.yml ↗ · job deploy copies this template to wrangler.ci.toml, then deploys it. CI edits and optional resource removal can change the final bindings.
cfchat · default
Entrypoint: worker/src/index.js
Build: npm run schema:generate
Static assets: ./frontend/dist · single-page-application · Worker first: true
Cron triggers (UTC): */15 * * * *
DB→ D1SESSIONS→ KVFILES→ R2CHANNEL_ROOM→ Durable Objects · class ChannelRoomSCHEDULER→ Durable Objects · class SchedulerUSER_INBOX→ Durable Objects · class UserInboxINSTANCE_BRIDGE→ Durable Objects · class InstanceBridgeASSETS→ Static assets
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L344 · fetch handler exported · calls createSiteEntry
- L345 · scheduled handler exported · calls rescueBridgeDeliveries, rescueTelegramNotifications, shouldRunDailyGc, tasks.push, runScheduledGc, pruneTelegramNotifications, ctx.waitUntil, Promise.all
- L61 · app.use("/api/*")
- L73 · app.use("/api/*")
- L79 · app.get("/api/health")
- L81 · app.get("/api/site")
- L89 · app.get("/api/register-links/:token")
- L111 · app.post("/api/register-links/:token/register")
- L147 · app.post("/api/auth/login")
- L169 · app.use("/api/*")
- L171 · app.get("/api/auth/session")
- L199 · app.post("/api/auth/logout")
- L206 · app.post("/api/auth/change-password")
- L261 · app.get("/api/users")
- L267 · app.get("/api/bootstrap")
- L279 · app.use("/api/admin/*")
- L295 · app.get("/api/ws/:kind/:id")
- L312 · app.get("/api/inbox/ws")
- L352 · shouldRunDailyGc calls (conditional paths may differ): time.getUTCHours, time.getUTCMinutes
Environment references: c.env.MAX_FILE_SIZE · c.env.DB
- L5 · toBase64Url calls (conditional paths may differ): join, Array.from, String.fromCharCode, replace, btoa
- L10 · fromBase64Url calls (conditional paths may differ): replace, value.replace, repeat, atob, Uint8Array.from, char.charCodeAt
- L17 · timingSafeEqual calls (conditional paths may differ): encoder.encode
- L30 · hashPassword calls (conditional paths may differ): toBase64Url, crypto.getRandomValues, crypto.subtle.importKey, encoder.encode, crypto.subtle.deriveBits, fromBase64Url
- L55 · verifyPassword calls (conditional paths may differ): hashPassword, timingSafeEqual
- L60 · toSessionVersion calls (conditional paths may differ): Number, Number.isFinite
- L65 · parseAdminUsernames calls (conditional paths may differ): filter, map, split, String, toLowerCase, username.trim
- L74 · isConfiguredAdminUsername calls (conditional paths may differ): toLowerCase, trim, String, Boolean, includes, parseAdminUsernames
- L80 · isAdminUser calls (conditional paths may differ): Boolean, Number
- L84 · resolveSessionTtl calls (conditional paths may differ): Date.parse, String, Number.isFinite, Math.max, Math.ceil, Date.now
- L92 · putSession calls (conditional paths may differ): env.SESSIONS.put, JSON.stringify, resolveSessionTtl
- L99 · createSession calls (conditional paths may differ): toBase64Url, crypto.getRandomValues, Number, encodeURIComponent, isAdminUser, toSessionVersion, putSession
- L117 · getSession calls (conditional paths may differ): env.SESSIONS.get, JSON.parse
- L136 · deleteSession calls (conditional paths may differ): env.SESSIONS.delete
Environment references: env.ADMIN_USERNAMES · env.SESSIONS
- L4 · mapVisibleChannel calls (conditional paths may differ): Number, publicFileUrl, Boolean
- L24 · mapAdminChannel calls (conditional paths may differ): Number, Boolean, publicFileUrl
- L43 · listVisibleChannels calls (conditional paths may differ): Number, all, bind, db.prepare, results.map
- L103 · listAdminChannels calls (conditional paths may differ): all, db.prepare, results.map, mapAdminChannel
- L121 · listChannelMembers calls (conditional paths may differ): all, bind, db.prepare, Number, results.map, publicFileUrl, Boolean, isUserDisabled
- L3 · mapUserDm calls (conditional paths may differ): Number, publicFileUrl, Boolean
- L21 · mapAdminDm calls (conditional paths may differ): Number
- L31 · listUserDms calls (conditional paths may differ): Number, all, bind, db.prepare, results.map
- L73 · listAdminDms calls (conditional paths may differ): all, db.prepare, results.map
- L5 · toInvite calls (conditional paths may differ): Number, Math.max
- L25 · listActiveRegistrationInvites calls (conditional paths may differ): all, db.prepare, results.map
- L49 · createRegistrationInvite calls (conditional paths may differ): run, bind, db.prepare, toInvite, toISOString
- L71 · revokeRegistrationInvite calls (conditional paths may differ): run, bind, db.prepare
- L82 · getAvailableRegistrationInvite calls (conditional paths may differ): first, bind, db.prepare, Number, Math.max
- L108 · createUserWithRegistrationInvite calls (conditional paths may differ): db.batch, bind, db.prepare, Number, String, message.includes
- L3 · getSiteSettings calls (conditional paths may differ): all, db.prepare, Object.fromEntries, results.map, String, siteIconUrlFromStored
- L16 · updateSiteSettings calls (conditional paths may differ): statements.push, bind, db.prepare, trim, String, normalizeSiteIconForStorage, db.batch, getSiteSettings
- L4 · mapUserSummary calls (conditional paths may differ): Number, publicFileUrl
- L13 · getUserProfile calls (conditional paths may differ): all, bind, db.prepare, activeUserSql, mapUserSummary
- L22 · mapAdminUser calls (conditional paths may differ): mapUserSummary, projectUserBan
- L30 · getUserByUsername calls (conditional paths may differ): all, bind, db.prepare
- L44 · isUserActiveById calls (conditional paths may differ): all, bind, db.prepare, activeUserSql, Number, Boolean
- L59 · listActiveUsers calls (conditional paths may differ): all, bind, db.prepare, activeUserSql, Number, results.map
- L74 · listContacts calls (conditional paths may differ): all, db.prepare, activeUserSql, results.map
- L87 · listAdminUsers calls (conditional paths may differ): all, db.prepare, results.map
- L99 · listStorageOwners calls (conditional paths may differ): all, db.prepare, results.map, Number, Boolean
- L5 · authMiddleware calls (conditional paths may differ): cookieRequestAllowed, errorResponse, extractSessionToken, validateSession, pathname.startsWith, v1ErrorResponse, errorCodeForStatus, c.set, next
- L20 · adminMiddleware calls (conditional paths may differ): c.get, pathname.startsWith, v1ErrorResponse, errorResponse, next
- L21 · registerAdminRoutes calls (conditional paths may differ): app.get, errorResponse, searchParams.get, c.env.FILES.list, summarizeR2Objects, listStorageOwners, c.header, c.json, Promise.all, listAdminUsers, listAdminChannels, listAdminDms, getSiteSettings, app.patch, parseJsonRequest, trim, String, updateSiteSettings, isR2ObjectUnavailableError, listActiveRegistrationInvites
Environment references: c.env.FILES · c.env.DB
- L4 · registerMaintenanceRoutes calls (conditional paths may differ): app.get, c.header, c.json, runSystemCheck
- L5 · registerStealthRoutes calls (conditional paths may differ): app.get, getStealthSettings, c.json, app.put, parseJsonRequest, errorResponse, saveStealthSettings, c.header, sessionCookie, c.get
Environment references: c.env.DB
- L6 · privateResponse calls (conditional paths may differ): Object.entries, headers.set, headers.delete
- L15 · assets calls (conditional paths may differ): notFound, env.ASSETS.fetch, response.headers.get, privateResponse, extractSessionToken, validateSession, response.text, html.replace
- L35 · integration calls (conditional paths may differ): request.headers.has, dispatch, response.headers.has, privateResponse, notFound
- L49 · createSiteEntry calls (conditional paths may differ): getStealthSettings, path.startsWith, dispatch, privateResponse, assets, submitGate, integration, extractSessionToken, validateSession, includes, gatePage, notFound, cookieRequestAllowed, path.slice, console.error
Environment references: env.ASSETS · env.DB
- L5 · loginWithPassword calls (conditional paths may differ): getUserByUsername, isUserDisabled, verifyPassword, createSession
Environment references: env.DB
- L7 · cookieToken calls (conditional paths may differ): cookieName, find, map, split, request.headers.get, part.trim, part.startsWith
- L13 · sessionCookie calls (conditional paths may differ): cookieName
- L18 · extractSessionToken calls (conditional paths may differ): request.headers.get, header.startsWith, trim, header.slice, searchParams.get, cookieToken
- L24 · cookieRequestAllowed calls (conditional paths may differ): request.headers.get, searchParams.get, cookieToken, includes
- L13 · payload calls (conditional paths may differ): readLimitedText, JSON.parse, bridgeError
- L18 · registerInstanceBridgePublicRoutes calls (conditional paths may differ): app.post, receiveBridgeRequest, c.json, console.error, JSON.stringify
- L29 · registerInstanceBridgeRoutes calls (conditional paths may differ): app.get, c.header, getChannelById, Number, c.req.param, then, authorizeRoom, c.get, bridgeError, first, c.json, publicBinding, app.use, requireAdmin, next, expireInvites, adminState, app.post, payload, createInvite
Environment references: c.env.DB
- L17 · envelope calls (conditional paths may differ): ownEndpoint, peerEndpoint, Date.now, crypto.randomUUID
- L23 · failureCode calls (conditional paths may differ): allowed.includes
- L31 · deliverBinding calls (conditional paths may differ): getBinding, identity, bindingSecret, Date.now, run, bind, env.DB.prepare, send, invitationSecret, envelope, Boolean, backoff, failureCode, first, decryptMessageContent, String, env.DB.batch, includes
- L104 · nextDeliveryAt calls (conditional paths may differ): first, Date.now
- L113 · wakeBinding calls (conditional paths may differ): env.INSTANCE_BRIDGE.get, env.INSTANCE_BRIDGE.idFromName, stub.fetch, createInternalHeaders, JSON.stringify
- L122 · wakeForMessage calls (conditional paths may differ): isLocalBridgeMessage, first, wakeBinding, console.warn, JSON.stringify
- L134 · rescueBridgeDeliveries calls (conditional paths may differ): expireInvites, Date.now, env.DB.batch, bind, env.DB.prepare, all, Promise.all, map, results.slice, wakeBinding
Environment references: env.DB · env.INSTANCE_BRIDGE
- L26 · normalizeMemberIds calls (conditional paths may differ): Array.isArray, filter, source.map, Number, Number.isFinite
- L36 · ensureValidInvitees calls (conditional paths may differ): join, userIds.map, all, bind, db.prepare, activeUserSql, results.map, Number
- L56 · registerChannelRoutes calls (conditional paths may differ): app.get, c.get, ensureGeneralChannelMembership, listVisibleChannels, c.json, channels.filter, app.post, parseJsonRequest, trim, String, errorResponse, includes, isReservedGeneralChannelName, filter, normalizeMemberIds, ensureValidInvitees, catch, run, bind, c.env.DB.prepare
Environment references: c.env.DB
- L4 · registerContactRoutes calls (conditional paths may differ): app.get, listContacts, c.json
Environment references: c.env.DB
- L7 · registerDmRoutes calls (conditional paths may differ): app.get, c.get, listUserDms, c.json, app.post, parseJsonRequest, Number, Number.isFinite, errorResponse, all, bind, c.env.DB.prepare, activeUserSql, Promise.all, ensureDmChannel, getUserBlockStatus, encodeURIComponent, listAdminDms
Environment references: c.env.DB
- L7 · registerMessageRoutes calls (conditional paths may differ): app.get, c.get, c.req.query, Number, sanitizeLimit, isRoomKind, Number.isInteger, errorResponse, authorizeRoom, Promise.all, listMessages, getPinnedMessage, markRoomRead, c.json, app.post, parseJsonRequest, String
Environment references: c.env.DB
- L24 · isInlineContentType calls (conditional paths may differ): contentType.startsWith
- L43 · contentDispositionValue calls (conditional paths may differ): sanitizeFilename, slice, trim, replace, safeUtf8.replace, encodeURIComponent
- L53 · validateUpload calls (conditional paths may differ): Number, Math.round, normalizeContentType, BLOCKED_MIME_TYPES.has, filter, map, split, String, item.trim, allowed.some, contentType.startsWith
- L74 · registerUploadRoutes calls (conditional paths may differ): app.post, errorResponse, c.get, Number, requestBodyTooLarge, Math.round, c.req.formData, formData.get, saveUploadedFile, c.json, String, message.startsWith, app.get, decodeURIComponent, c.req.param, c.req.header, authorization.startsWith, trim, authorization.slice, searchParams.get
- L164 · saveUploadedFile calls (conditional paths may differ): validateUpload, getUploadedFileByClientId, file.name.includes, file.name.slice, file.name.lastIndexOf, Date.now, crypto.randomUUID, sanitizeFilename, normalizeContentType, encryptAttachment, file.arrayBuffer, env.FILES.put, recordUploadedFile, env.FILES.delete, console.warn, includes, String, encodeURIComponent
Environment references: env.MAX_FILE_SIZE · env.ALLOWED_FILE_TYPES · c.env.FILES · c.env.MAX_FILE_SIZE · c.env.DB · env.DB · env.FILES
- L12 · generateSchemaManifest calls (conditional paths may differ): initSqlJs, replaceAll, read, db.exec, collectSchemaArtifacts, result.values.map, Object.fromEntries, result.columns.map, JSON.parse, digest, update, createHash, sort, D1_MIGRATIONS.map, migrationChecksums, db.close
- L28 · escapeSql calls (conditional paths may differ): replace, String
- L32 · main calls (conditional paths may differ): hashPassword, escapeSql, trim, resolve, process.cwd, mkdirSync, writeFileSync, console.log
Environment references: process.env.EDGECHAT_ADMIN_USERNAME · process.env.CFCHAT_ADMIN_USERNAME · process.env.EDGECHAT_ADMIN_PASSWORD · process.env.CFCHAT_ADMIN_PASSWORD · process.env.EDGECHAT_ADMIN_DISPLAY_NAME · process.env.CFCHAT_ADMIN_DISPLAY_NAME
- L20 · setOutput calls (conditional paths may differ): appendFileSync, String
- L26 · apiError calls (conditional paths may differ): Array.isArray, join, payload.errors.map
- L31 · listWorkerSecrets calls (conditional paths may differ): fetch, encodeURIComponent, catch, response.json, apiError, Array.isArray
- L44 · createKeyring calls (conditional paths may differ): JSON.stringify, toString, randomBytes
- L51 · createAutomaticKey calls (conditional paths may differ): toString, randomBytes
- L59 · writeSecretsFile calls (conditional paths may differ): Object.values, console.log, mkdirSync, dirname, writeFileSync, JSON.stringify
- L67 · prepareWorkerEncryptionSecret calls (conditional paths may differ): listWorkerSecrets, filter, secrets.map, map, AUTO_KEY_SECRET_PATTERN.exec, Number, secretNames.has, setOutput, console.log, loadEncryptionKeyring, writeSecretsFile, Math.max, createAutomaticKey
Environment references: process.env.GITHUB_OUTPUT · process.env.EDGECHAT_WORKER_NAME · process.env.EDGECHAT_SECRETS_FILE · process.env.EDGECHAT_ENCRYPTION_KEYRING · process.env.EDGECHAT_APPLY_ENCRYPTION_KEYRING · process.env.EDGECHAT_ROTATE_ENCRYPTION_KEY
- L31 · extractApiError calls (conditional paths may differ): Array.isArray, join, payload.errors.map
- L38 · queryD1 calls (conditional paths may differ): fetch, JSON.stringify, response.json, extractApiError
- L57 · main calls (conditional paths may differ): generateSchemaManifest, process.argv.includes, inspectSchema, JSON.stringify, console.log, collectSchemaArtifacts, schemaTables, collectAppliedMigrations, buildD1MigrationPlan, readFileSync, resolve, mkdirSync, dirname, writeFileSync
Environment references: process.env.CLOUDFLARE_API_TOKEN · process.env.CLOUDFLARE_ACCOUNT_ID · process.env.EDGECHAT_D1_DATABASE_ID
- L33 · resourceNames calls (conditional paths may differ): readEnv
- L47 · setOutput calls (conditional paths may differ): String, appendFileSync, console.log
- L55 · extractApiError calls (conditional paths may differ): Array.isArray, join, payload.errors.map
- L73 · cloudflareRequest calls (conditional paths may differ): Object.entries, url.searchParams.set, String, fetchImpl, JSON.stringify, response.text, JSON.parse
- L147 · listD1Databases calls (conditional paths may differ): cloudflareRequest, Array.isArray, payload.result.map, all.push, Number
- L174 · listKvNamespaces calls (conditional paths may differ): cloudflareRequest, Array.isArray, payload.result.map, all.push
- L200 · listR2Buckets calls (conditional paths may differ): cloudflareRequest, Array.isArray, buckets.map
- L211 · isR2NotEnabledError calls (conditional paths may differ): Number
- L219 · hasR2Access calls (conditional paths may differ): cloudflareRequest, isR2NotEnabledError, console.warn
- L239 · ensureD1Database calls (conditional paths may differ): listD1Databases, databases.find, console.log, cloudflareRequest
- L262 · ensureKvNamespace calls (conditional paths may differ): listKvNamespaces, namespaces.find, console.log, cloudflareRequest
- L290 · ensureR2Bucket calls (conditional paths may differ): hasR2Access, listR2Buckets, buckets.find, console.log, cloudflareRequest
- L309 · ensureCloudflareResources calls (conditional paths may differ): resourceNames, console.log, ensureD1Database, ensureKvNamespace, ensureR2Bucket, setOutput
Environment references: process.env.GITHUB_OUTPUT
- L5 · targetUserId calls (conditional paths may differ): Number, c.req.param
- L9 · registerUserBlockRoutes calls (conditional paths may differ): app.put, c.get, targetUserId, Number.isInteger, errorResponse, getUserBlockStatus, setUserBlocked, c.json, app.delete
Environment references: c.env.DB
- L8 · registerUserProfileRoutes calls (conditional paths may differ): app.get, parseLocalUserId, c.req.param, errorResponse, getUserProfile, c.json, app.patch, c.get, parseJsonRequest, Array.isArray, Object.hasOwn, payload.displayName.trim, updates.push, binds.push, validateBio, resolveAvatarKeyUpdate, run, bind, c.env.DB.prepare, updates.join
Environment references: c.env.DB
- L30 · webhookUrl calls (conditional paths may differ): url.toString
- L37 · telegramApiError calls (conditional paths may differ): errorResponse
- L46 · avatarCacheKey calls (conditional paths may differ): url.toString
- L53 · missingAvatarResponse calls (conditional paths may differ): errorResponse, response.headers.set
- L59 · telegramAvatarResponse calls (conditional paths may differ): String
- L72 · registerTelegramPublicRoutes calls (conditional paths may differ): app.get, String, c.req.param, test, missingAvatarResponse, externalSenderExists, avatarCacheKey, cache.match, getTelegramCredentials, errorResponse, loadTelegramUserAvatar, telegramAvatarResponse, cache.put, response.clone, app.post, c.req.header, parseJsonRequest, consumeTelegramNotificationLink, c.executionCtx.waitUntil, catch
- L147 · registerTelegramNotificationRoutes calls (conditional paths may differ): app.get, c.json, getTelegramNotificationState, c.get, app.post, createTelegramNotificationLink, errorResponse, app.put, parseJsonRequest, updateTelegramNotificationPreferences, app.delete, disconnectTelegramNotifications
- L170 · registerTelegramAdminRoutes calls (conditional paths may differ): app.get, c.json, listTelegramBridgeAdminState, app.put, parseJsonRequest, trim, String, errorResponse, getTelegramBot, randomToken, webhookUrl, setTelegramWebhook, saveTelegramBridgeConfig, c.get, telegramApiError, app.post, Number, Number.isInteger, test, getTelegramCredentials
Environment references: c.env.DB
- L36 · isUuid calls (conditional paths may differ): UUID_PATTERN.test, trim, String
- L40 · validRoomRequest calls (conditional paths may differ): isRoomKind, Number.isInteger
- L44 · requireRoom calls (conditional paths may differ): String, c.req.param, Number, validRoomRequest, c.get, authorizeRoom
- L58 · legacyProxyRequest calls (conditional paths may differ): includes, request.arrayBuffer, url.toString
- L69 · convertLegacyError calls (conditional paths may differ): response.headers.get, contentType.includes, catch, json, response.clone, v1ErrorResponse, errorCodeForStatus
- L82 · registerV1Routes calls (conditional paths may differ): app.get, getSiteSettings, c.json, Number, Boolean, app.post, parseJsonRequest, trim, String, v1ErrorResponse, getUserByUsername, isUserDisabled, verifyPassword, createMobileDeviceSession, refreshMobileDeviceSession, revokeMobileDeviceSession, c.get, issueRealtimeTicket, validRoomRequest, authorizeRoom
Environment references: c.env.DB · c.env.MAX_FILE_SIZE · c.env.MESSAGE_RETENTION_DAYS · c.env.FILES
- L1 · normalizeContentType calls (conditional paths may differ): toLowerCase, trim, split, String
- L8 · sanitizeFilename calls (conditional paths may differ): join, filter, Array.from, replaceAll, trim, String, character.codePointAt, cleaned.slice
- L23 · safeFilenameExtension calls (conditional paths may differ): sanitizeFilename, cleanName.lastIndexOf, toLowerCase, cleanName.slice, test
- L34 · normalizeAudioAttachmentMetadata calls (conditional paths may differ): startsWith, normalizeContentType, Math.min, Math.max, Math.round, Number, Array.isArray, map, payload.waveform.slice
- L9 · resolveAvatarKeyUpdate calls (conditional paths may differ): Object.hasOwn, trim, String, getOwnedUploadedFileMetadata, startsWith
- L1 · ensureDmChannel calls (conditional paths may differ): join, sort, Number, all, bind, db.prepare, run, db.batch
- L3 · normalizeMentionUserIds calls (conditional paths may differ): Array.isArray, filter, values.map, Number.isInteger
- L10 · isMentionBoundary calls (conditional paths may differ): test
- L14 · contentMentionsUsername calls (conditional paths may differ): String, text.indexOf, isMentionBoundary
- L33 · resolveMessageMentionUserIds calls (conditional paths may differ): filter, normalizeMentionUserIds, Number, join, candidates.map, all, bind, db.prepare, activeUserSql, map, results.filter, contentMentionsUsername
- L3 · getPinnedMessage calls (conditional paths may differ): all, bind, env.DB.prepare, Number, getMessageById
- L15 · pinMessage calls (conditional paths may differ): run, bind, db.prepare, Number
- L35 · unpinMessage calls (conditional paths may differ): run, bind, db.prepare, Number
Environment references: env.DB
- L1 · normalizeReplyMessageId calls (conditional paths may differ): Number, Number.isInteger
- L12 · mapReplyReference calls (conditional paths may differ): Number
- L21 · resolveMessageReply calls (conditional paths may differ): normalizeReplyMessageId, all, bind, db.prepare, Number, mapReplyReference
- L41 · findMessageReplyBySource calls (conditional paths may differ): all, bind, db.prepare, Number, String, mapReplyReference
- L59 · getMessageSourceReference calls (conditional paths may differ): normalizeReplyMessageId, all, bind, db.prepare, Number, String
- L7 · toNullableNumber calls (conditional paths may differ): Number, Number.isFinite
- L12 · mapAttachment calls (conditional paths may differ): toNullableNumber, publicFileUrl, JSON.parse
- L31 · mapReplyAttachment calls (conditional paths may differ): toNullableNumber, publicFileUrl
- L47 · mapReplySender calls (conditional paths may differ): String, Number, publicFileUrl
- L66 · mapMessage calls (conditional paths may differ): map, JSON.parse, Number, String, publicFileUrl, Boolean, isUserDisabled, normalizeMentionUserIds, mapAttachment
- L122 · externalSenderExists calls (conditional paths may differ): all, bind, db.prepare, String, Boolean
- L138 · mapDecryptedMessage calls (conditional paths may differ): decryptMessageContent, Number, mapReplySender, mapReplyAttachment, mapMessage
- L213 · getMessageDeletionTarget calls (conditional paths may differ): all, bind, db.prepare, Number
- L229 · listMessages calls (conditional paths may differ): Number, filters.push, binds.push, all, bind, env.DB.prepare, filters.join, reverse, Promise.all, results.map, mapDecryptedMessage
- L243 · getMessageById calls (conditional paths may differ): all, bind, env.DB.prepare, Number, mapDecryptedMessage
- L251 · getMessageBySource calls (conditional paths may differ): all, bind, env.DB.prepare, String, mapDecryptedMessage
- L263 · getMessageByClientId calls (conditional paths may differ): all, bind, env.DB.prepare, Number, String, mapDecryptedMessage
- L278 · hasConsumedClientMessageId calls (conditional paths may differ): all, bind, db.prepare, Number, String, Boolean
- L291 · getRoomSyncCursor calls (conditional paths may differ): all, bind, db.prepare, Number
- L310 · getRoomCompactedCursor calls (conditional paths may differ): all, bind, db.prepare, Number
- L323 · listRoomMessageEvents calls (conditional paths may differ): Number, getRoomCompactedCursor, Math.min, Math.max, all, bind, env.DB.prepare, results.slice, events.push, getMessageById
- L375 · softDeleteMessage calls (conditional paths may differ): run, bind, db.prepare, Number
- L389 · persistMessage calls (conditional paths may differ): Number, pickAttachment, trim, String, fileBelongsToUser, encryptMessageContent, JSON.stringify, normalizeMentionUserIds, run, bind, env.DB.prepare, getMessageById, includes, all, isR2ObjectUnavailableError, getMessageBySource, getMessageByClientId, hasConsumedClientMessageId
- L536 · insertMessage calls (conditional paths may differ): persistMessage
- L559 · insertMessageIdempotent calls (conditional paths may differ): persistMessage
- L563 · insertExternalMessage calls (conditional paths may differ): persistMessage
Environment references: env.DB
- L13 · getStealthSettings calls (conditional paths may differ): all, db.prepare, JSON.parse, isGateAppearance, createGateAppearance, run, bind, JSON.stringify, getStealthSettings
- L33 · saveStealthSettings calls (conditional paths may differ): getStealthSettings, randomToken, crypto.getRandomValues, createGateAppearance, run, bind, db.prepare, JSON.stringify
- L6 · mapMapping calls (conditional paths may differ): Number, String, Boolean
- L20 · listTelegramBridgeAdminState calls (conditional paths may differ): Promise.all, all, env.DB.prepare, Boolean, channelsResult.results.map, Number, mappingsResult.results.map
- L62 · getTelegramCredentials calls (conditional paths may differ): all, env.DB.prepare, Promise.all, decryptSecretValue
- L86 · saveTelegramBridgeConfig calls (conditional paths may differ): first, env.DB.prepare, Promise.all, encryptSecretValue, run, bind, String, Number
- L128 · getTelegramMappingByChatId calls (conditional paths may differ): all, bind, db.prepare, String, mapMapping
- L146 · listEnabledTelegramMappingsForChannel calls (conditional paths may differ): all, bind, db.prepare, Number, results.map
- L164 · createTelegramMapping calls (conditional paths may differ): all, bind, db.prepare, Number, run, String
- L197 · updateTelegramMapping calls (conditional paths may differ): run, bind, db.prepare, Number
- L208 · deleteTelegramMapping calls (conditional paths may differ): run, bind, db.prepare, Number
Environment references: env.DB
- L32 · sendSocketError calls (conditional paths may differ): ws.send, JSON.stringify
- L40 · getMessageByteLength calls (conditional paths may differ): encode, ArrayBuffer.isView
- L55 · normalizeWebSocketMessage calls (conditional paths may differ): decode, ArrayBuffer.isView
Environment references: env.DB
- L8 · getChannelRoomStub calls (conditional paths may differ): Number, env.CHANNEL_ROOM.get, env.CHANNEL_ROOM.idFromName
- L13 · getUserInboxStub calls (conditional paths may differ): Number, env.USER_INBOX.get, env.USER_INBOX.idFromName
- L18 · forwardVerifiedRequest calls (conditional paths may differ): Object.entries, url.searchParams.set, String, createVerifiedPrincipalHeaders, includes, request.arrayBuffer, stub.fetch, url.toString
- L44 · forwardRoomConnection calls (conditional paths may differ): forwardVerifiedRequest, getChannelRoomStub
- L54 · forwardInboxConnection calls (conditional paths may differ): forwardVerifiedRequest, getUserInboxStub
- L63 · notifyUserInbox calls (conditional paths may differ): fetch, getUserInboxStub, createInternalHeaders, JSON.stringify
- L72 · submitClientRoomAction calls (conditional paths may differ): forwardVerifiedRequest, getChannelRoomStub, JSON.stringify
- L85 · submitExternalRoomMessage calls (conditional paths may differ): fetch, getChannelRoomStub, createInternalHeaders, JSON.stringify
Environment references: env.CHANNEL_ROOM · env.USER_INBOX
- L3 · resolveReadableMessageId calls (conditional paths may differ): Number, filters.push, binds.push, all, bind, db.prepare, filters.join
- L17 · markRoomRead calls (conditional paths may differ): resolveReadableMessageId, run, bind, db.prepare, Number
- L32 · countUnreadMessages calls (conditional paths may differ): all, bind, db.prepare, Number
- L46 · countUnreadAttention calls (conditional paths may differ): all, bind, db.prepare, Number
- L81 · listRoomMemberIds calls (conditional paths may differ): all, bind, db.prepare, activeUserSql, Number, filter, results.map, Number.isFinite
- L1 · isR2ObjectUnavailableError calls (conditional paths may differ): String, message.includes
- L9 · recordUploadedFile calls (conditional paths may differ): run, bind, db.prepare, String, Number
- L36 · mapUploadedFile calls (conditional paths may differ): Number, encodeURIComponent
- L48 · getUploadedFileByClientId calls (conditional paths may differ): all, bind, db.prepare, Number, String, mapUploadedFile
- L61 · getUploadedFileMetadata calls (conditional paths may differ): all, bind, db.prepare, String, Number
- L79 · getOwnedUploadedFileMetadata calls (conditional paths may differ): all, bind, db.prepare, String, Number
- L103 · fileBelongsToUser calls (conditional paths may differ): all, bind, db.prepare, String, Number, Boolean
- L119 · canAccessFile calls (conditional paths may differ): String, all, bind, db.prepare, Number.isFinite, Number, Boolean
- L14 · bytesToBase64 calls (conditional paths may differ): String.fromCharCode, bytes.subarray, btoa
- L23 · base64ToBytes calls (conditional paths may differ): String, test, atob, Uint8Array.from, character.charCodeAt
- L39 · getKeyringSource calls (conditional paths may differ): String, sort, filter, map, Object.entries, exec, Number, left.localeCompare, JSON.stringify
- L67 · parseLegacyKeyring calls (conditional paths may differ): JSON.parse, String, KEY_ID_PATTERN.test, Array.isArray, Object.entries
- L90 · addKey calls (conditional paths may differ): KEY_ID_PATTERN.test, base64ToBytes, keys.set, crypto.subtle.importKey
- L107 · loadEncryptionKeyring calls (conditional paths may differ): getKeyringSource, parseLegacyKeyring, KEY_ID_PATTERN.test, addKey, keys.has
- L139 · getCryptoKey calls (conditional paths may differ): keyring.keys.get
- L147 · messageAad calls (conditional paths may differ): encoder.encode, Number
- L152 · externalMessageAad calls (conditional paths may differ): encoder.encode, Number, String
- L157 · attachmentAad calls (conditional paths may differ): encoder.encode, String
- L162 · secretAad calls (conditional paths may differ): encoder.encode, String
- L167 · isEncryptedMessageContent calls (conditional paths may differ): value.startsWith
- L174 · getMessageEnvelopeKeyId calls (conditional paths may differ): isEncryptedMessageContent, value.split, KEY_ID_PATTERN.test
- L182 · encryptMessageContent calls (conditional paths may differ): String, loadEncryptionKeyring, crypto.getRandomValues, getCryptoKey, Boolean, crypto.subtle.encrypt, externalMessageAad, messageAad, encoder.encode, bytesToBase64
- L214 · decryptMessageContent calls (conditional paths may differ): String, isEncryptedMessageContent, content.split, KEY_ID_PATTERN.test, loadEncryptionKeyring, base64ToBytes, content.startsWith, crypto.subtle.decrypt, externalMessageAad, messageAad, getCryptoKey, decoder.decode, error.message.startsWith
- L263 · encryptSecretValue calls (conditional paths may differ): String, loadEncryptionKeyring, crypto.getRandomValues, crypto.subtle.encrypt, secretAad, getCryptoKey, encoder.encode, bytesToBase64
- L282 · decryptSecretValue calls (conditional paths may differ): String, encrypted.startsWith, encrypted.split, KEY_ID_PATTERN.test, loadEncryptionKeyring, base64ToBytes, crypto.subtle.decrypt, secretAad, getCryptoKey, decoder.decode, error.message.startsWith
- L315 · toBytes calls (conditional paths may differ): ArrayBuffer.isView
- L328 · isEncryptedAttachment calls (conditional paths may differ): toBytes, FILE_MAGIC.every
- L336 · getAttachmentEnvelopeKeyId calls (conditional paths may differ): toBytes, isEncryptedAttachment, decoder.decode, bytes.subarray, KEY_ID_PATTERN.test
- L355 · encryptAttachment calls (conditional paths may differ): toBytes, loadEncryptionKeyring, encoder.encode, crypto.getRandomValues, crypto.subtle.encrypt, attachmentAad, getCryptoKey, envelope.set
- L384 · decryptAttachment calls (conditional paths may differ): toBytes, isEncryptedAttachment, getAttachmentEnvelopeKeyId, bytes.subarray, loadEncryptionKeyring, crypto.subtle.decrypt, attachmentAad, getCryptoKey, error.message.startsWith
- L3 · submitExternalMessage calls (conditional paths may differ): insertExternalMessage, JSON.stringify
- L43 · toPositiveInteger calls (conditional paths may differ): Number, Number.isFinite, Math.floor
- L49 · getGcConfig calls (conditional paths may differ): toPositiveInteger, Math.min, filter, map, split, String, value.trim
- L94 · createExecutionBudget calls (conditional paths may differ): canSpend
- L173 · safeErrorMessage calls (conditional paths may differ): slice, String
- L177 · placeholders calls (conditional paths may differ): join, Array.from
- L181 · valueRows calls (conditional paths may differ): join, Array.from
- L185 · uniqueKeys calls (conditional paths may differ): filter, keys.map, trim, String
- L195 · resultChanges calls (conditional paths may differ): Number
- L199 · loadSiteIconGuard calls (conditional paths may differ): budget.spend, all, db.prepare, classifyStoredSiteIcon
- L214 · findReferencedKeys calls (conditional paths may differ): uniqueKeys, budget.spend, all, bind, db.prepare, valueRows, results.map, String, unique.includes, referenced.add
- L242 · reserveR2DeleteKeys calls (conditional paths may differ): uniqueKeys, budget.spend, run, bind, db.prepare, valueRows, resultChanges
- L257 · removePendingR2DeleteKeys calls (conditional paths may differ): uniqueKeys, budget.spend, run, bind, db.prepare, placeholders, resultChanges
- L271 · completeR2Delete calls (conditional paths may differ): budget.spend, db.batch, bind, db.prepare
- L280 · retryDelayMinutes calls (conditional paths may differ): Math.min, Math.max
- L285 · markR2RetryFailure calls (conditional paths may differ): budget.spend, run, bind, db.prepare
- L301 · processR2Rows calls (conditional paths may differ): findReferencedKeys, rows.map, removePendingR2DeleteKeys, String, referenced.has, budget.canSpend, budget.markExhausted, budget.spend, env.FILES.delete, completeR2Delete, Number, markR2RetryFailure, retryDelayMinutes, safeErrorMessage
- L361 · runRetryQueueStep calls (conditional paths may differ): Math.min, budget.remainingR2Operations, Math.floor, budget.remainingInternalOperations, budget.remainingD1Statements, budget.markExhausted, budget.spend, all, bind, env.DB.prepare, processR2Rows
- L386 · runMobileProtocolCleanupStep calls (conditional paths may differ): budget.spend, run, bind, env.DB.prepare, env.DB.batch, map, changes.some
- L450 · runExpiredMessagesStep calls (conditional paths may differ): budget.spend, env.DB.batch, bind, env.DB.prepare, resultChanges
- L481 · runHardDeleteInvitesStep calls (conditional paths may differ): budget.spend, run, bind, env.DB.prepare, resultChanges
- L502 · runHardDeleteChannelsStep calls (conditional paths may differ): budget.canSpend, budget.markExhausted, budget.spend, Math.min, Math.floor, all, bind, env.DB.prepare, hardDeleteChannels, results.map, Number
- L533 · runHardDeleteUsersStep calls (conditional paths may differ): budget.canSpend, budget.markExhausted, Math.min, Math.floor, budget.spend, all, bind, env.DB.prepare, results.map, Number, placeholders, run, resultChanges, env.DB.batch
- L585 · runOrphanedUploadsStep calls (conditional paths may differ): budget.canSpend, budget.markExhausted, budget.spend, all, bind, env.DB.prepare, reserveR2DeleteKeys, results.map
- L610 · runScheduledGc calls (conditional paths may differ): getGcConfig, createExecutionBudget, createSummary, loadSiteIconGuard, steps.map, steps.entries, step, runOrphanedUploadsStep, runRetryQueueStep, budget.snapshot, console.log, JSON.stringify
- L660 · cleanupR2Keys calls (conditional paths may differ): getGcConfig, createExecutionBudget, createSummary, loadSiteIconGuard, uniqueKeys, keys.slice, findReferencedKeys, chunk.filter, referenced.has, reserveR2DeleteKeys, budget.spend, all, bind, env.DB.prepare, placeholders, processR2Rows, budget.snapshot
Environment references: env.MESSAGE_RETENTION_DAYS · env.SOFT_DELETE_RETENTION_DAYS · env.GC_BATCH_SIZE · env.GC_MAX_BATCHES_PER_RUN · env.R2_DELETE_MAX_RETRY · env.ORPHAN_UPLOAD_RETENTION_DAYS · env.GC_INTERNAL_OPERATION_BUDGET · env.GC_D1_STATEMENT_BUDGET · env.GC_R2_OPERATION_BUDGET · env.SITE_ORIGINS · env.DB · env.FILES
- L21 · validId calls (conditional paths may differ): test
- L25 · normalizeOrigin calls (conditional paths may differ): bridgeError, isIP, host.replace, test
- L39 · publicIp calls (conditional paths may differ): isIP, map, value.split, includes, test
- L52 · validatePublicDns calls (conditional paths may differ): normalizeOrigin, Promise.all, map, fetcher, AbortSignal.timeout, bridgeError, response.json, filter, answers.flat, ips.some, publicIp
- L67 · hmacKey calls (conditional paths may differ): crypto.subtle.importKey, encoder.encode
- L71 · sign calls (conditional paths may differ): crypto.subtle.sign, hmacKey, encoder.encode, join, Array.from, padStart, b.toString
- L76 · verify calls (conditional paths may differ): test, Uint8Array.from, Number.parseInt, signature.slice, crypto.subtle.verify, hmacKey, encoder.encode
- L82 · validateEndpoint calls (conditional paths may differ): validId, Number.isSafeInteger, endpoint.channelName.trim, bridgeError, normalizeOrigin
- L89 · validateEnvelope calls (conditional paths may differ): Date.now, includes, validId, Number.isSafeInteger, Math.abs, bridgeError, validateEndpoint
- L99 · readLimitedText calls (conditional paths may differ): response.body.getReader, reader.read, bridgeError, chunks.push, reader.cancel, bytes.set, decode
- L119 · signedResponse calls (conditional paths may differ): JSON.stringify, sign
- L127 · sendEnvelope calls (conditional paths may differ): normalizeOrigin, checkDns, JSON.stringify, fetcher, AbortSignal.timeout, sign, readLimitedText, verify, response.headers.get, bridgeError, JSON.parse
- L11 · createInvite calls (conditional paths may differ): managedGroup, identity, crypto.randomUUID, randomToken, Date.now, expireInvites, run, bind, env.DB.prepare, encryptInvitationSecret, includes, String, bridgeError, JSON.stringify
- L33 · acceptInvite calls (conditional paths may differ): managedGroup, JSON.parse, bridgeError, validId, test, Number.isSafeInteger, Date.now, validateEndpoint, identity, expireInvites, getBinding, run, bind, env.DB.prepare, encryptBindingSecret, randomToken, encryptInvitationSecret, crypto.randomUUID, includes, String
- L67 · changeBinding calls (conditional paths may differ): getBinding, bridgeError, managedGroup, Date.now, run, bind, env.DB.prepare, env.DB.batch
- L99 · expireInvites calls (conditional paths may differ): Date.now, run, bind, env.DB.prepare
- L110 · receiveClaim calls (conditional paths may differ): validId, Date.now, bridgeError, test, identity, bindingSecret, includes, run, bind, env.DB.prepare, encryptBindingSecret, getBinding
- L130 · receiveControl calls (conditional paths may differ): Number.isSafeInteger, Number, bridgeError, includes, Date.now, managedGroup, env.DB.batch, bind, env.DB.prepare
- L160 · receiptExists calls (conditional paths may differ): Boolean, first
Environment references: env.DB
- L10 · receiveBridgeRequest calls (conditional paths may differ): readLimitedText, JSON.parse, bridgeError, validateEnvelope, getBinding, invitationSecret, bindingSecret, verify, request.headers.get, identity, receiveClaim, receiveControl, validId, Number.isSafeInteger, Number, Date.now, test, senderName.trim, content.trim, encode
Environment references: env.DB
- L37 · first calls (conditional paths may differ): all, bind, db.prepare
- L42 · identity calls (conditional paths may differ): run, bind, env.DB.prepare, crypto.randomUUID, normalizeOrigin, first, bridgeError
- L60 · getBinding calls (conditional paths may differ): first
- L72 · bindingSecret calls (conditional paths may differ): decryptSecretValue
- L75 · encryptBindingSecret calls (conditional paths may differ): encryptSecretValue
- L78 · invitationSecret calls (conditional paths may differ): decryptSecretValue
- L81 · encryptInvitationSecret calls (conditional paths may differ): encryptSecretValue
- L85 · requireAdmin calls (conditional paths may differ): first, isUserDisabled, bridgeError
- L89 · managedGroup calls (conditional paths may differ): requireAdmin, authorizeChannelManagement, bridgeError
- L96 · publicBinding calls (conditional paths may differ): Boolean, Number
- L106 · adminState calls (conditional paths may differ): Promise.all, all, env.DB.prepare, first, bindings.map
Environment references: env.DB
- L21 · logBridgeFailure calls (conditional paths may differ): console.warn, JSON.stringify
- L25 · escapeTelegramHtml calls (conditional paths may differ): replaceAll, String
- L33 · formatTelegramMessage calls (conditional paths may differ): escapeTelegramHtml
- L39 · splitTelegramFormattedMessage calls (conditional paths may differ): Array.from, String, formatTelegramMessage, escapeTelegramHtml, chunks.push
- L59 · telegramMediaKind calls (conditional paths may differ): contentType.startsWith
- L67 · sendTextMessage calls (conditional paths may differ): splitTelegramFormattedMessage, chunks.entries, sendTelegramText
- L78 · sendMessageToTelegram calls (conditional paths may differ): sendTextMessage, loadEdgeChatAttachment, logBridgeFailure, Number, splitTelegramFormattedMessage, sendTelegramMedia, telegramMediaKind, captions.slice, sendTelegramText
- L129 · telegramReplyMessageId calls (conditional paths may differ): reference.sourceMessageId.startsWith, Number, reference.sourceMessageId.slice, Number.isInteger
- L137 · forwardEdgeChatMessageToTelegram calls (conditional paths may differ): isGroupChannelKind, isLocalBridgeMessage, Promise.all, getTelegramCredentials, listEnabledTelegramMappingsForChannel, getMessageSourceReference, Promise.resolve, mappings.map, sendMessageToTelegram, telegramReplyMessageId, logBridgeFailure, Number, String
- L184 · ingestTelegramMessage calls (conditional paths may differ): getMessageBySource, findMessageReplyBySource, importTelegramAttachment, join, filter, submitExternalRoomMessage, response.json, deleteImportedTelegramAttachment, catch
Environment references: env.DB
- L10 · largestPhotoSize calls (conditional paths may differ): photoSizes.reduce, Number
- L23 · avatarContentType calls (conditional paths may differ): toLowerCase, String, path.endsWith
- L30 · loadTelegramUserAvatar calls (conditional paths may differ): getTelegramUserProfilePhotos, largestPhotoSize, getTelegramFile, Number, downloadTelegramFile, avatarContentType
- L11 · validateBotToken calls (conditional paths may differ): trim, String, test
- L19 · parseTelegramResponse calls (conditional paths may differ): catch, response.json
- L27 · callTelegramApi calls (conditional paths may differ): validateBotToken, fetch, AbortSignal.timeout, JSON.stringify, parseTelegramResponse
- L41 · callTelegramMultipartApi calls (conditional paths may differ): validateBotToken, fetch, parseTelegramResponse
- L50 · getTelegramBot calls (conditional paths may differ): callTelegramApi
- L54 · setTelegramWebhook calls (conditional paths may differ): callTelegramApi
- L63 · getTelegramChat calls (conditional paths may differ): callTelegramApi, String
- L67 · getTelegramUserProfilePhotos calls (conditional paths may differ): callTelegramApi, Number
- L75 · sendTelegramText calls (conditional paths may differ): String, Number, callTelegramApi
- L93 · getTelegramFile calls (conditional paths may differ): callTelegramApi, String
- L97 · downloadTelegramFile calls (conditional paths may differ): validateBotToken, replace, String, fetch, Number, response.headers.get, response.arrayBuffer
- L118 · sendTelegramMedia calls (conditional paths may differ): formData.set, String, JSON.stringify, Number, Math.round, callTelegramMultipartApi
- L8 · tokenHash calls (conditional paths may differ): encode, join, Array.from, crypto.subtle.digest, padStart, byte.toString
- L15 · getTelegramNotificationState calls (conditional paths may differ): Promise.all, getTelegramCredentials, first, bind, env.DB.prepare, Boolean
- L31 · createTelegramNotificationLink calls (conditional paths may differ): getTelegramCredentials, test, randomToken, tokenHash, run, bind, env.DB.prepare
- L47 · consumeTelegramNotificationLink calls (conditional paths may differ): String, exec, trim, tokenHash, join, filter, first, bind, env.DB.prepare, activeUserSql, slice, Boolean
- L69 · updateTelegramNotificationPreferences calls (conditional paths may differ): run, bind, db.prepare, Number
- L78 · disconnectTelegramNotifications calls (conditional paths may differ): run, bind, db.prepare
- L83 · enqueueTelegramNotification calls (conditional paths may differ): run, bind, env.DB.prepare, slice, String, first, deliverTelegramNotification
- L100 · deliverTelegramNotification calls (conditional paths may differ): first, bind, env.DB.prepare, Promise.all, getTelegramCredentials, activeUserSql, run, sendTelegramText, console.warn, String
- L146 · rescueTelegramNotifications calls (conditional paths may differ): all, bind, env.DB.prepare, Promise.all, results.map, deliverTelegramNotification
- L155 · pruneTelegramNotifications calls (conditional paths may differ): run, env.DB.prepare
Environment references: env.DB
- L17 · telegramObjectKey calls (conditional paths may differ): safeFilenameExtension, crypto.randomUUID
- L22 · importTelegramAttachment calls (conditional paths may differ): getTelegramFile, Number, downloadTelegramFile, sanitizeFilename, normalizeContentType, telegramObjectKey, encryptAttachment, env.FILES.put
- L76 · loadEdgeChatAttachment calls (conditional paths may differ): Number, env.FILES.get, decryptAttachment, object.arrayBuffer, sanitizeFilename, normalizeContentType
- L110 · deleteImportedTelegramAttachment calls (conditional paths may differ): env.FILES.delete, console.warn, JSON.stringify, String
Environment references: env.FILES
- L18 · d1 calls (conditional paths may differ): db.prepare, statement.bind, statement.step, rows.push, statement.getAsObject, db.getRowsModified, statement.free, execute, makeBound
Environment references: env.DB
- L3 · durableObjectHealth calls (conditional paths may differ): isVerifiedInternalRequest, Response.json
- L1 · displayName calls (conditional paths may differ): trim, join, filter
- L6 · parseAttachment calls (conditional paths may differ): Array.isArray, message.photo.at, String, Number
- L63 · parseTelegramMessageUpdate calls (conditional paths may differ): Number.isInteger, Number, parseAttachment, String, content.trim, displayName
- L14 · schemaTables calls (conditional paths may differ): map, manifest.artifacts.filter, artifact.startsWith, artifact.slice
- L18 · collectSchemaArtifacts calls (conditional paths may differ): query, objects.map, map, objects.filter, String, table.replaceAll, artifacts.add
- L32 · collectAppliedMigrations calls (conditional paths may differ): artifacts.has, query, rows.map, String
- L38 · assessSchema calls (conditional paths may differ): manifest.artifacts.filter, artifacts.has, map, manifest.migrations.filter, applied.has, recognizedMigrations.filter, applied.get, migration.compatibleChecksums.includes, recognizedMigrations.map, sort, filter, applied.keys, knownIds.has, manifest.migrations.at, at
- L63 · inspectSchema calls (conditional paths may differ): collectSchemaArtifacts, schemaTables, collectAppliedMigrations, assessSchema
- L31 · probe calls (conditional paths may differ): Date.now, Promise.race, operation, setTimeout, resolve, clearTimeout
- L54 · inspectEnvironment calls (conditional paths may differ): trim, present, some, Object.keys, test, map
- L64 · checkObject calls (conditional paths may differ): absent, probe, namespace.get, namespace.idFromName, stub.fetch, createInternalHeaders, response.json
- L78 · runSystemCheck calls (conditional paths may differ): Date.now, absent, all, db.prepare, probe, query, inspectSchema, Promise.all, database, checkObject, inspectEnvironment, environment.some, toISOString, checks.some, includes, manifest.migrations.at
Environment references: env.DB · env.SESSIONS · env.FILES · env.CHANNEL_ROOM · env.USER_INBOX · env.SCHEDULER · env.INSTANCE_BRIDGE
- L12 · createMessageDeletion calls (conditional paths may differ): Number, Number.isInteger, authorize, getDeletionTarget, persistDeletion, catch, then, Promise.resolve, cleanupAttachments, console.warn, JSON.stringify
Environment references: env.DB · env.FILES
- L15 · createMessageSubmission calls (conditional paths may differ): resolveDmBlockStatus, Number, Promise.all, resolveMentions, resolveReply, persistMessage, JSON.stringify
Environment references: env.DB
- L11 · normalizeMessageId calls (conditional paths may differ): Number, Number.isInteger
- L19 · requirePinPermission calls (conditional paths may differ): authorize
- L27 · createMessagePinning calls (conditional paths may differ): normalizeMessageId, requirePinPermission, persistPin, loadPinnedMessage, JSON.stringify
- L55 · createMessageUnpinning calls (conditional paths may differ): normalizeMessageId, requirePinPermission, persistUnpin, JSON.stringify
Environment references: env.DB
- L9 · toSqlTimestamp calls (conditional paths may differ): replace, date.toISOString
- L13 · refreshExpiry calls (conditional paths may differ): toSqlTimestamp, Date.now
- L17 · publicExpiry calls (conditional paths may differ): replace, String
- L21 · normalizeDevice calls (conditional paths may differ): trim, String, slice
- L31 · hashOpaqueToken calls (conditional paths may differ): encode, String, crypto.subtle.digest, join, Array.from, String.fromCharCode, replace, btoa
- L38 · createAccessSession calls (conditional paths may differ): randomToken, toISOString, Date.now, Number, encodeURIComponent, isAdminUser, putSession
- L58 · sessionResponse calls (conditional paths may differ): publicExpiry
- L68 · createMobileDeviceSession calls (conditional paths may differ): normalizeDevice, crypto.randomUUID, randomToken, hashOpaqueToken, refreshExpiry, Number, env.DB.batch, bind, env.DB.prepare, createAccessSession, sessionResponse
- L110 · refreshMobileDeviceSession calls (conditional paths may differ): trim, String, hashOpaqueToken, all, bind, env.DB.prepare, isUserDisabled, Number, randomToken, refreshExpiry, run, createAccessSession, sessionResponse
- L180 · revokeMobileDeviceSession calls (conditional paths may differ): run, bind, env.DB.prepare, String, Number, deleteSession
- L193 · updateCurrentDeviceSessionVersion calls (conditional paths may differ): run, bind, env.DB.prepare, Number, String
Environment references: env.DB
- L13 · issueRealtimeTicket calls (conditional paths may differ): randomToken, hashOpaqueToken, encryptSecretValue, ticketContext, replace, toISOString, Date.now, run, bind, env.DB.prepare, Number, String, expiresAt.replace
- L48 · consumeRealtimeTicket calls (conditional paths may differ): trim, String, hashOpaqueToken, all, bind, env.DB.prepare, decryptSecretValue, ticketContext, validateSession, Number
Environment references: env.DB
- L4 · toNumber calls (conditional paths may differ): Number, Number.isFinite
- L9 · validateSession calls (conditional paths may differ): getSession, all, bind, env.DB.prepare, isUserDisabled, deleteSession, String, toNumber, isAdminUser, putSession
Environment references: env.DB
- L9 · isRoomKind calls (conditional paths may differ): ROOM_KINDS.has
- L13 · normalizePrincipal calls (conditional paths may differ): Boolean, Number, Number.isInteger
- L25 · getChannelById calls (conditional paths may differ): Number, Number.isInteger, all, bind, db.prepare
- L44 · getChannelMembership calls (conditional paths may differ): Number, Number.isInteger, all, bind, db.prepare
- L69 · authorizeRoom calls (conditional paths may differ): normalizePrincipal, Number, isRoomKind, Number.isInteger, db.prepare, statement.bind, bound.all
- L109 · authorizeChannelManagement calls (conditional paths may differ): normalizePrincipal, getChannelById, getChannelMembership
- L135 · authorizeMessageModeration calls (conditional paths may differ): authorizeRoom, getChannelMembership
- L3 · decodeFilePath calls (conditional paths may differ): startsWith, String, slice, decodeURIComponent
- L23 · normalizedOrigins calls (conditional paths may differ): filter, origins.map, absoluteHttpUrl, trim, String
- L31 · siteIconUrlFromStored calls (conditional paths may differ): trim, String, stored.startsWith, stored.slice, encodeURIComponent
- L38 · normalizeSiteIconForStorage calls (conditional paths may differ): trim, String, raw.startsWith, decodeFilePath, absoluteHttpUrl, has, normalizedOrigins
- L61 · classifyStoredSiteIcon calls (conditional paths may differ): trim, String, stored.startsWith, stored.slice, decodeFilePath, absoluteHttpUrl, normalizedOrigins, origins.has
- L10 · createGateAppearance calls (conditional paths may differ): crypto.getRandomValues, Object.fromEntries, map, Object.entries, replace, btoa, String.fromCharCode, bytes.slice, id
- L20 · isGateAppearance calls (conditional paths may differ): every, Object.entries, Number.isInteger, test
- L48 · escapeHtml calls (conditional paths may differ): value.replace
- L52 · renderGate calls (conditional paths may differ): input, escapeHtml, row, join, styles.slice
- L3 · storageOwnerFromObjectKey calls (conditional paths may differ): String, USER_OBJECT_KEY_PATTERN.exec, Number, Number.isSafeInteger, normalizedKey.startsWith
- L20 · summarizeR2Objects calls (conditional paths may differ): storageOwnerFromObjectKey, summaries.get, Math.max, Number, Number.isNaN, uploadedAt.getTime, uploadedAt.toISOString, summaries.set, summaries.values
- L20 · gatePage calls (conditional paths may differ): renderGate
- L34 · readForm calls (conditional paths may differ): request.headers.get, request.body.getReader, reader.read, reader.cancel, chunks.push, reader.releaseLock, text
- L58 · submitGate calls (conditional paths may differ): request.headers.get, notFound, readForm, gatePage, loginWithPassword, sessionCookie
- L10 · isUserDisabled calls (conditional paths may differ): Date.now, Number, Date.parse, Number.isFinite
- L19 · banExpiryFromMinutes calls (conditional paths may differ): Date.now, toISOString
- L23 · projectUserBan calls (conditional paths may differ): Date.now, Boolean, Number, Date.parse, Number.isFinite
- L9 · logProjectionFailure calls (conditional paths may differ): console.warn, JSON.stringify
- L13 · createUnreadProjection calls (conditional paths may differ): includes, Number, Promise.all, countUnread, countMentions, Promise.resolve, notifyInbox, slice, String, logFailure, enqueueNotification, listMemberIds, memberIds.filter, recipientIds.map, notifyRecipient
- L21 · notifyRecipient calls (conditional paths may differ): includes, Number, Promise.all, countUnread, countMentions, Promise.resolve, notifyInbox, slice, String, logFailure, enqueueNotification
Environment references: env.DB
- L3 · jsonResponse calls (conditional paths may differ): JSON.stringify
- L13 · errorResponse calls (conditional paths may differ): jsonResponse
- L17 · v1ErrorResponse calls (conditional paths may differ): jsonResponse
- L33 · requestBodyTooLarge calls (conditional paths may differ): Number, request.headers.get, Number.isFinite
- L38 · parseJsonRequest calls (conditional paths may differ): catch, request.json
- L42 · sanitizeLimit calls (conditional paths may differ): Number, Number.isFinite, Math.min
- L50 · keyBelongsToOwner calls (conditional paths may differ): Number, Number.isFinite, startsWith, String
- L59 · pickAttachment calls (conditional paths may differ): String, keyBelongsToOwner, Number, encodeURIComponent, normalizeAudioAttachmentMetadata
- L84 · publicFileUrl calls (conditional paths may differ): encodeURIComponent
- L88 · nextDailyUtcHour calls (conditional paths may differ): target.setUTCMinutes, target.setUTCHours, target.setUTCDate, target.getUTCDate
- L98 · randomToken calls (conditional paths may differ): crypto.getRandomValues, join, Array.from, String.fromCharCode, replace, btoa
- L7 · isVerifiedInternalRequest calls (conditional paths may differ): request.headers.get
- L11 · createInternalHeaders calls (conditional paths may differ): headers.set
- L17 · createVerifiedPrincipalHeaders calls (conditional paths may differ): createInternalHeaders, headers.set, String, Date.now
- L25 · parseVerifiedPrincipal calls (conditional paths may differ): isVerifiedInternalRequest, request.headers.get, Number, Number.isFinite
- L51 · parseVerifiedUserId calls (conditional paths may differ): parseVerifiedPrincipal
Build and deployment pipeline · 5 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push, pull_request, workflow_dispatch
verify · no job dependencies declared
- Checkout
actions/checkout@v5 - Validate Gradle wrapper
gradle/actions/wrapper-validation@v4 - Setup Java
actions/setup-java@v5 - Setup Gradle
gradle/actions/setup-gradle@v4 - Test, lint and build debug APKs
./gradlew testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest --stacktrace - Upload debug APK
actions/upload-artifact@v4
Triggers: push, workflow_dispatch
release · no job dependencies declared
- Checkout
actions/checkout@v5 - Validate Gradle wrapper
gradle/actions/wrapper-validation@v4 - Setup Node.js
actions/setup-node@v5 - Setup Java
actions/setup-java@v5 - Setup Android SDK
android-actions/setup-android@v3 - Install Android SDK 36
sdkmanager "platforms;android-36" "build-tools;36.0.0" - Setup Gradle
gradle/actions/setup-gradle@v4 - Resolve release version
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then version="${GITHUB_REF_NAME#android-v}" tag="$GITHUB_REF_NAME" else version="${INPUT_VERSION:-$(node -p "require('./package.json').version")}" tag="android-v${version}" fi [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" - Configure release signing
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do test -n "${!name}" done printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > capacitor/android/release.keystore cat > capacitor/android/signing.properties <<EOF storeFile=release.keystore storePassword=$ANDROID_KEYSTORE_PASSWORD keyAlias=$ANDROID_KEY_ALIAS keyPassword=$ANDROID_KEY_PASSWORD EOF - Install dependencies
npm ci - Run Node tests
npm test - Build and sync bundled Web UI
npm run capacitor:sync - Test, lint and build signed artifacts
node capacitor/scripts/run-gradle.mjs :app:testDebugUnitTest :app:assembleDebugAndroidTest :app:lintRelease :app:assembleRelease :app:bundleRelease -PedgechatVersionName=${{ steps.version.outputs.version }} -PedgechatVersionCode=${{ github.run_number }} --stacktrace - Prepare checksums
mkdir -p capacitor/release-artifacts cp capacitor/android/app/build/outputs/apk/release/app-release.apk capacitor/release-artifacts/edgechat-${{ steps.version.outputs.version }}.apk cp capacitor/android/app/build/outputs/bundle/release/app-release.aab capacitor/release-artifacts/edgechat-${{ steps.version.outputs.version }}.aab cd capacitor/release-artifacts sha256sum *.apk *.aab > SHA256SUMS.txt - Verify APK signature
apksigner_path="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -n 1)" test -n "$apksigner_path" "$apksigner_path" verify --verbose --print-certs capacitor/release-artifacts/*.apk # AAB 使用 JAR 签名格式;这里只校验签名完整性,不把 ZIP 结构警告升级为失败。 jarsigner -verify -verbose -certs capacitor/release-artifacts/*.aab - Publish GitHub Release
prerelease_flag="" if [[ "$PRERELEASE" == "true" ]]; then prerelease_flag="--prerelease"; fi if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then git tag "$RELEASE_TAG" "$GITHUB_SHA" git push origin "$RELEASE_TAG" fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" capacitor/release-artifacts/* --clobber else gh release create "$RELEASE_TAG" capacitor/release-artifacts/* \ --title "EdgeChat Android ${{ steps.version.outputs.version }}" \ --generate-not… - Remove signing material
rm -f capacitor/android/release.keystore capacitor/android/signing.propertiesCondition: always()
Triggers: push, pull_request, workflow_dispatch
verify · no job dependencies declared
- Checkout
actions/checkout@v5 - Setup Node.js
actions/setup-node@v5 - Setup Java
actions/setup-java@v5 - Setup Android SDK
android-actions/setup-android@v3 - Install Android SDK 36
sdkmanager "platforms;android-36" "build-tools;36.0.0" - Validate Gradle wrappers
gradle/actions/wrapper-validation@v4 - Install dependencies
npm ci - Run Node tests
npm test - Test, lint and build Capacitor debug APK
npm run build:capacitor - Upload Capacitor debug APK
actions/upload-artifact@v4
Triggers: workflow_dispatch
deploy · no job dependencies declared
- Checkout
actions/checkout@v5 - Setup Node.js
actions/setup-node@v5 - Install dependencies
npm ci - Run tests
npm test - Build demo assets
npm run build:demo - Deploy isolated demo worker
npx wrangler deploy --config wrangler.demo.toml
Triggers: push, workflow_dispatch
deploy · no job dependencies declared
- Checkout
actions/checkout@v5 - Setup Node.js
actions/setup-node@v5 - Install dependencies
npm ci - Run tests
npm test - Build frontend assets
npm run build - Ensure Cloudflare resources
node .github/scripts/ensure-cloudflare-resources.mjs - Generate wrangler config for CI
cp wrangler.example.toml wrangler.ci.toml sed -i "s|YOUR_D1_DATABASE_ID_HERE|${{ steps.ensure_resources.outputs.d1_database_id }}|g" wrangler.ci.toml sed -i "s|YOUR_KV_NAMESPACE_ID_HERE|${{ steps.ensure_resources.outputs.kv_namespace_id }}|g" wrangler.ci.toml if [[ "$R2_AVAILABLE" == "true" ]]; then sed -i "/^\[\[r2_buckets\]\]$/,/^$/ s|^bucket_name = \".*\"$|bucket_name = \"${R2_BUCKET_NAME}\"|" wrangler.ci.toml else sed -i '/^\[\[r2_buckets\]\]$/,/^$/d' wrangler.ci.toml fi - Initialize D1 schema (first creation only)
npx wrangler d1 execute ${{ steps.ensure_resources.outputs.d1_database_name }} --remote --file worker/schema.sql --config wrangler.ci.tomlCondition: steps.ensure_resources.outputs.d1_created == 'true' - Prepare D1 migrations
node .github/scripts/prepare-d1-migrations.mjs - Apply D1 migrations
npx wrangler d1 execute ${{ steps.ensure_resources.outputs.d1_database_name }} --remote --file .tmp/edgechat-d1-migrations.sql --config wrangler.ci.toml - Verify D1 schema contract
node .github/scripts/prepare-d1-migrations.mjs --verify - Generate admin bootstrap SQL (optional)
if [[ -n "$EDGECHAT_ADMIN_USERNAME" && -n "$EDGECHAT_ADMIN_PASSWORD" ]]; then node .github/scripts/generate-admin-bootstrap-sql.mjs else echo "Skipping admin bootstrap SQL generation; admin credentials were not provided." fi - Ensure admin user (optional)
npx wrangler d1 execute ${{ steps.ensure_resources.outputs.d1_database_name }} --remote --file .tmp/edgechat-admin-upsert.sql --config wrangler.ci.tomlCondition: hashFiles('.tmp/edgechat-admin-upsert.sql') != '' - Prepare Worker encryption secret
node .github/scripts/prepare-worker-encryption-secret.mjs - Deploy worker
if [[ -f .tmp/worker-secrets.json ]]; then npx wrangler deploy --config wrangler.ci.toml --secrets-file .tmp/worker-secrets.json else npx wrangler deploy --config wrangler.ci.toml fi - Remove temporary secret files
rm -f .tmp/worker-secrets.jsonCondition: always()
prebuild: npm run schema:generateprebuild:frontend: npm run vditor:assetsprebuild:demo: npm run schema:generate && npm run vditor:assetsprebuild:capacitor:web: npm run vditor:assetsbuild:frontend: vite build --config frontend/vite.config.jsbuild:demo: vite build --config frontend/vite.demo.config.jsbuild:capacitor:web: vite build --config frontend/vite.capacitor.config.jsbuild: npm run build:frontendbuild:capacitor: npm run capacitor:sync && node capacitor/scripts/run-gradle.mjsdeploy: npm run build && wrangler deploydeploy:demo: npm run build:demo && wrangler deploy --config wrangler.demo.toml
Repository README
View original on GitHub ↗Full upstream document by @aozorae · README.md · snapshot 848c95b
自己的聊天空间,不必从维护服务器开始。
基于 Cloudflare 的开源自部署团队聊天系统
实时群聊与私信 · Telegram 双向桥接 · 语音与文件 · 消息与附件服务端加密
EdgeChat 是一个运行在 Cloudflare 上的开源团队聊天系统。 你可以用它为团队、项目或小圈子搭建一个独立的聊天空间:讨论放进群组,私事留给私信,语音和文件也能随手分享。
它不需要你另外维护一台常驻服务器。应用与数据资源部署在你自己的 Cloudflare 账号下,通过 GitHub Actions 自动部署和更新。如果成员已经在使用 Telegram,也可以把两侧群组连接起来,继续使用各自习惯的聊天入口。
界面预览 · 在线演示 · Telegram 桥接 · 功能特性 · 隐私与加密 · 部署 · 本地开发
界面预览
| 聊天界面 | 管理后台 |
![]() |
![]() |
在线演示
先看看界面,再决定是否部署。
演示站复用正式项目的 Vue 页面、路由、状态管理和实时消息逻辑,但 API、WebSocket、文件上传与 Telegram 回流均在浏览器内存中模拟,不是连接真实后端的多人聊天室。
刷新页面或点击右上角「重置演示数据」即可恢复初始状态。演示操作不会访问正式 Worker,也不会写入 D1、KV 或 R2。
为什么是 EdgeChat
EdgeChat 面向这样一种需求:想拥有一个自己的聊天空间,又不想为此长期维护服务器、数据库和一整套运行环境。
| 你关心的事情 | EdgeChat 的做法 |
|---|---|
| 部署和维护 | 基于 Cloudflare Workers,无需另行维护常驻服务器;支持 GitHub Actions 自动部署 |
| 成员与管理 | 提供独立账号、公开与私有群组、私信和管理后台 |
| 已有的 Telegram 群 | 通过 Bot 双向桥接,让两侧成员继续使用各自习惯的入口 |
| 自己修改与扩展 | 源码开放,可以按自己的需求调整界面和功能 |
项目本身免费开源。云服务费用取决于 Cloudflare 的套餐、资源配置和实际用量。
跨实例群组绑定
从 2.8.0 起,两个独立 EdgeChat 站点的管理员可以在后台左侧栏「跨实例群组绑定」中选择公开、私有或 general 群组,以 10 分钟一次性邀请 → 对端选群认领 → 发起方核对并确认 的流程建立双向文字同步。每群最多绑定一个远端群,私信不可绑定。
只同步生效后的本站原创纯文字及昵称、来源实例;不回填历史,不同步附件、引用关系、编辑或撤回。暂停会取消积压,恢复只同步新世代消息;解绑后不再启动新投递,在途消息可能完成,已送达副本不会自动删除。后台可查看队列、失败原因与丢弃计数。
正常 Actions 部署会先应用数据库迁移,再发布带 INSTANCE_BRIDGE DO 的 Worker,无需新增手动 Secret,必须保留现有加密 keyring。详情见 绑定操作与部署指南 和 协议与可靠性约定。
Telegram 双向桥接
2.8.0起,Telegram 与实例桥接均只转发本站账号原创消息;来自另一条桥的消息不会继续跨桥扩散。
不用把所有人都搬进同一个应用。
管理员可以在后台查看 EdgeChat 的公开与私有群组,并将任一群组与 Telegram 群组绑定。Telegram Bot 会双向转发消息:在网页里发出的消息可以同步到 Telegram,Telegram 群里的消息也会回到 EdgeChat;一对一私信不会进入桥接。
适合已经有 Telegram 群、同时又需要一个独立网页聊天入口的团队和社区。
两个聊天入口,同一段讨论。
功能特性
💬 聊天与会话
- 公开群组、私有群组和一对一私信。
- 实时消息、历史消息分页、语音消息与文件分享。
- 消息回复、引用跳转、@ 提及与回复提醒。
- 私信联系人拉黑与解除拉黑;拉黑期间双方均无法继续发送私信。
- 支持定时硬删除过期消息。
🎨 日常使用体验
- Liquid Glass 风格界面,适配桌面和移动端。
- 语音录制、波形进度与倍速播放。
- 网页前台提供站内消息通知;后台可使用浏览器系统通知,需要用户授权。
- 用户可在设置页绑定管理员配置的 Telegram Bot,在关闭网页后接收私信和群聊 @ 提醒,并分别开关两类通知。提醒不包含消息正文;Telegram 暂时失败时会有限重试。
- 文件上传、头像管理与基础无障碍支持。
🛠 实例管理
- 仪表盘、用户管理、注册邀请与网站设置。
- 支持永久封禁和按天、小时、分钟设置临时封禁;临时封禁到期后自动恢复,无需额外定时任务。
- 在浏览器端直接比对源码仓库,检查当前部署是否有更新。
🔌 连接与扩展
- 公开与私有群组均可由管理员配置 Telegram 双向消息桥接,并支持语音消息同步。
- WebMCP 站点工具:在兼容的客户端环境中,提供登录、查询会话、读取与发送消息等能力。
隐私与加密
EdgeChat 对新写入的消息正文和新上传的附件使用 AES-256-GCM 服务端静态加密。历史明文数据保持原状,读取时兼容新旧数据,不会在部署或后台任务中批量回填加密。
管理员后台不提供群组或私信消息正文的查看入口,仍可查看消息数量等聚合统计。
[!NOTE] 这是服务端加密,不是端到端加密。 Worker 在通过会话权限校验后解密内容,Cloudflare 运行环境和掌握密钥的部署方仍需要被信任。后台没有消息查看入口,不代表部署者在技术上无法访问内容。
开启 Telegram 桥接后,被转发的消息也会进入对应的 Telegram 会话。
密钥管理与轮换说明
GitHub Actions 会管理服务端加密 Worker Secrets。首次部署时,如果目标 Worker 尚无加密 Secret,工作流会自动生成随机 32 字节 AES 密钥,以独立的版本化 Secret 注入,并记录当前 active key ID。后续普通部署只检查这些 Secret 是否存在,不会重新生成、覆盖或轮换。
生产环境已经存在的 EDGECHAT_ENCRYPTION_KEYRING JSON 密钥环会被原样保留并继续兼容。
需要手动指定密钥时,可创建名为 EDGECHAT_ENCRYPTION_KEYRING 的 GitHub Repository Secret:
{"activeKeyId":"v1","keys":{"v1":"BASE64_ENCODED_32_BYTE_KEY"}}
首次部署会直接采用该值。
已有 Worker 需要自动增量轮换时,手动运行 Deploy Worker 并勾选 rotate_encryption_key。工作流只新增一个版本化密钥 Secret,并把 active key ID 切换到新版本;所有旧 Secret 和旧 JSON 密钥环都保持不变。新消息使用新密钥,旧密文继续使用各自信封中的 key ID 解密。
apply_encryption_keyring 是备用的手动覆盖入口。使用时,Repository Secret 中必须是完整 JSON 密钥环,keys 需要保留所有仍被历史密文引用的旧 key ID,再增加新 key 并更新 activeKeyId。
删除旧 key 会导致对应历史密文永久无法读取。 apply_encryption_keyring 与 rotate_encryption_key 不能在同一次运行中同时启用。
技术栈
| 部分 | 技术 |
|---|---|
| 前端 | Vue 3、Vue Router、Vite |
| 后端 | Cloudflare Workers、Hono |
| 实时通信 | Durable Objects、WebSocket Hibernation |
| 数据库 | Cloudflare D1 |
| 会话存储 | Cloudflare KV |
| 文件存储 | Cloudflare R2 |
| 构建与部署 | Wrangler、GitHub Actions |
更多实现说明见 TECHNICAL.md。
部署
GitHub Actions 自动部署
推荐使用仓库内置的 GitHub Actions 工作流进行部署和后续更新。
按照文档完成 Cloudflare 授权与仓库配置后,可以手动运行 Deploy Worker,也可以通过向 master 或 main 分支推送代码触发部署。工作流文件为 .github/workflows/deploy-worker.yml。
手动部署与 Docker
本地手动部署的资源准备、配置方法和注意事项见 部署文档。
Docker 相关说明见 DOCKER.md。
Android 客户端
查看客户端、安装与构建说明
capacitor/ 客户端的 APK 内置 Vue Web UI,并通过少量 Kotlin 对接系统文件选择、通知、麦克风权限、通知会话跳转和外部链接。包名为 com.aozorae.edgechat.web,首次登录时填写自己的 EdgeChat HTTPS 服务地址,不绑定固定部署。
- 安装包:从 GitHub Releases 下载
edgechat-*.apk,并核对SHA256SUMS.txt。 - 首次使用:填写自己的 EdgeChat HTTPS 服务地址、账号和密码。
- 本地构建:准备 JDK 21 与 Android SDK 36 后,运行
npm run build:capacitor。 - Debug APK:
capacitor/android/app/build/outputs/apk/debug/app-debug.apk。 - CI:
.github/workflows/capacitor-android-ci.yml上传edgechat-capacitor-debug。 - 发布:推送
android-v*标签或手动运行Android Release,使用四个ANDROID_KEYSTORE_*Secrets 构建签名 APK/AAB。 - 服务切换:退出登录后可以修改地址;切换实例时自动清理旧实例令牌。
当前不包含 Room 离线数据库、WorkManager Outbox 或 FCM;应用停止接收消息时,不承诺后台即时通知。
原生 android/ Kotlin + Jetpack Compose 客户端暂时弃用,不再作为默认发行版。源码、API v1 契约与独立 CI 暂时保留。
完整说明见 Android 客户端文档。
本地开发
# 获取源码
git clone https://github.com/aozorae/Edgechat.git
cd Edgechat
# 安装依赖
npm install
# 启动纯前端演示,无需先部署云端资源
npm run dev:demo
连接实际后端进行开发或部署前,请先按照文档准备对应的 Cloudflare 资源与配置。
# 前端开发
npm run dev:frontend
# 本地构建
npm run build
# 本地手动发布
npm run deploy
演示站构建、部署与 CI 环境变量
# 独立构建演示站
npm run build:demo
# 部署独立演示 Worker
npm run deploy:demo
演示站使用 wrangler.demo.toml 和 .github/workflows/deploy-demo.yml,Worker 名称为 edgechat-demo。GitHub Actions 仅支持手动触发,并读取 DEMO_CLOUDFLARE_ACCOUNT_ID、DEMO_CLOUDFLARE_API_TOKEN,不会改变现有生产部署工作流。
在非交互环境下部署时,需要提前设置 CLOUDFLARE_API_TOKEN。
后台更新检查会在构建时自动记录当前 GitHub 仓库、分支和提交。为了获得准确结果,手动部署应在 Git 仓库内基于已经推送的干净提交构建;源码仓库需要保持公开,浏览器才能直接调用 GitHub Compare API,整个过程不会创建定时任务。
PowerShell 示例:
$env:CLOUDFLARE_API_TOKEN = "your-token"
npm run deploy
项目结构
查看主要目录
Edgechat/
├─ assets/previews/ # 界面预览
├─ frontend/ # Vue 前端与浏览器内演示
├─ worker/
│ ├─ schema.sql # 数据库结构
│ ├─ migrations/ # 数据库迁移
│ └─ src/ # API、认证与 Durable Objects
├─ capacitor/ # 基于 Web UI 的 Android 客户端
├─ android/ # 暂时保留的原生 Android 客户端
├─ .github/workflows/ # 自动部署与 CI
├─ wrangler.toml
├─ wrangler.demo.toml
├─ package.json
├─ README.md
├─ README.en.md
├─ README.ja.md
└─ LICENSE
交流与贡献
遇到问题、想讨论功能,或者有自己的使用方式,欢迎提交 Issue,也欢迎加入 Telegram 社区。
代码、文档、翻译与问题反馈都可以帮助项目继续完善。欢迎提交 Pull Request。
感谢所有为 EdgeChat 提供帮助的贡献者:
特别感谢 @fix221 提交 PWA 支持 PR #31。
Star History
协议说明
本项目采用 GNU GPL v3.0 or later,详见 LICENSE。
鸣谢
✨ 特别鸣谢
|
VenLac(Venlacy) 项目早期贡献了大量核心代码,为 EdgeChat 的整体架构奠定了基础; 同时凭借自身在社区中的影响力,为项目推广做出了突出贡献。让更多开发者认识并使用了 EdgeChat。 |
感谢 linux do 在推广方面为本项目做出的贡献。
免责声明
EdgeChat 是一个自部署的开源项目。项目维护者仅提供软件本身,不运营、控制或管理任何由用户自行部署的实例。
实例的部署者和使用者应自行对其部署方式、使用行为及其中产生的内容和数据负责。
项目维护者不对任何独立部署实例的使用或滥用承担责任。
Frequently asked about EdgeChat
What is EdgeChat?+
EdgeChat is a self-hosted Discord/Slack alternative built on the Cloudflare developer platform. Self-hosted team channels, direct messages and optional file sharing on Cloudflare
What does EdgeChat replace?+
EdgeChat is listed as an alternative to Discord, Slack. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does EdgeChat use?+
EdgeChat is built on D1, Durable Objects, KV, R2, Workers.
How much does EdgeChat cost to run?+
Budget for Workers Paid from US$5 per account/month plus usage above included allowances. The core uses Workers, D1, KV and SQLite Durable Objects; R2 is optional for attachments. Free-plan eligibility for this build has not been established by CPU testing. Password hashing uses PBKDF2 with 100,000 iterations. Worker Free has a 10ms CPU ceiling; authentication, encryption, upload and garbage-collection costs need deployment measurements before claiming free hosting. The deployment workflow generates wrangler.ci.toml from wrangler.example.toml, supplies resource IDs and may omit R2 when the account has not enabled it. The static demo configuration is a separate deployment. New messages and attachments use server-side encryption, not end-to-end encryption; trust the deployer and Cloudflare, preserve all historical encryption keys, and review optional Telegram/cross-instance disclosure. Workers Paid has a US$5 minimum per account/month; this is a pricing floor, not a measured total. Requests, CPU, DO duration, D1 rows/storage, KV operations, R2 and optional providers need a workload budget. No paid plan, Cloudflare resources or upstream deployment were created by this catalog review. Check current Cloudflare pricing before deploying.
Is EdgeChat open source?+
The upstream repository declares the GPL-3.0-or-later license. Read its terms at https://raw.githubusercontent.com/aozorae/Edgechat/848c95b32ae2dfa9981d1739fbc76d5e03ba3cee/LICENSE. Source code and contributor credit are available at https://github.com/aozorae/Edgechat.
Community rating
No ratings yet. Tried this project? Share your experience.
One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.





Discussion · 0
sign in to comment →