Merge branch 'master' into ep/p2p-group-signing

This commit is contained in:
Evgeny @ SimpleX Chat
2026-09-09 21:37:31 +00:00
26 changed files with 1926 additions and 144 deletions
@@ -31,11 +31,11 @@ func apiLoadMessages(
let chatModel = ChatModel.shared
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
let paginationIsInitial = switch pagination { case .initial: true; default: false }
let paginationIsLast = switch pagination { case .last: true; default: false }
// When openAroundItemId is provided, chatId can be different too
if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast && openAroundItemId == nil) || Task.isCancelled {
// For .initial allow the chatItems to be empty, as well as for .last that is used for searching
let allowEmptyItems = paginationIsInitial || paginationIsLast || openAroundItemId != nil
if chatWasSwitched(chat, pagination, openAroundItemId) || (!allowEmptyItems && chat.chatItems.isEmpty) || Task.isCancelled {
return
}
@@ -79,6 +79,7 @@ func apiLoadMessages(
newItems.insert(contentsOf: chat.chatItems, at: insertAt)
let newReversed: [ChatItem] = newItems.reversed()
await MainActor.run {
if chatWasSwitched(chat, pagination, openAroundItemId) { return }
im.reversedChatItems = newReversed
im.chatState.splits = modifiedSplits.newSplits
im.chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
@@ -99,6 +100,7 @@ func apiLoadMessages(
let new: [ChatItem] = newItems
let newReversed: [ChatItem] = newItems.reversed()
await MainActor.run {
if chatWasSwitched(chat, pagination, openAroundItemId) { return }
im.reversedChatItems = newReversed
im.chatState.splits = newSplits
im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new)
@@ -122,6 +124,7 @@ func apiLoadMessages(
let newReversed: [ChatItem] = newItems.reversed()
let orderedSplits = newSplits
await MainActor.run {
if chatWasSwitched(chat, pagination, openAroundItemId) { return }
im.reversedChatItems = newReversed
im.chatState.splits = orderedSplits
im.chatState.unreadAfterItemId = chat.chatItems.last!.id
@@ -147,6 +150,7 @@ func apiLoadMessages(
newItems.append(contentsOf: chat.chatItems)
let items = newItems
await MainActor.run {
if chatWasSwitched(chat, pagination, openAroundItemId) { return }
im.reversedChatItems = items.reversed()
im.chatState.splits = newSplits
if im.secondaryIMFilter == nil {
@@ -158,6 +162,14 @@ func apiLoadMessages(
}
/// .initial pagination and opening around item set ChatModel.chatId themselves after the items are loaded.
/// In other cases the chat could be switched while the items were loading, and the items of the previously opened chat
/// must not be added to the items of the currently opened one
private func chatWasSwitched(_ chat: Chat, _ pagination: ChatPagination, _ openAroundItemId: ChatItem.ID?) -> Bool {
let paginationIsInitial = switch pagination { case .initial: true; default: false }
return !paginationIsInitial && openAroundItemId == nil && ChatModel.shared.chatId != chat.id
}
private class ModifiedSplits {
let oldUnreadSplitIndex: Int
let newUnreadSplitIndex: Int
@@ -33,13 +33,21 @@ suspend fun apiLoadMessages(
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) = coroutineScope {
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), contentTag ?: chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
/** When [openAroundItemId] is provided, chatId can be different too */
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
// For .initial allow the chatItems to be empty, as well as for .last that is used for searching
val allowEmptyItems = pagination is ChatPagination.Initial || pagination is ChatPagination.Last || openAroundItemId != null
if (chatWasSwitched(chat, pagination, openAroundItemId)
|| (!allowEmptyItems && chat.chatItems.isEmpty())
|| !isActive) return@coroutineScope
processLoadedChat(chatsCtx, chat, navInfo, pagination, openAroundItemId, visibleItemIndexesNonReversed)
}
/** .initial pagination and opening around item set [ChatModel.chatId] themselves after the items are loaded.
* In other cases the chat could be switched while the items were loading, and the items of the previously opened chat
* must not be added to the items of the currently opened one */
private fun chatWasSwitched(chat: Chat, pagination: ChatPagination, openAroundItemId: Long?): Boolean =
pagination !is ChatPagination.Initial && openAroundItemId == null &&
(chatModel.chatId.value != chat.id || chatModel.remoteHostId() != chat.remoteHostId)
suspend fun processLoadedChat(
chatsCtx: ChatModel.ChatsContext,
chat: Chat,
@@ -94,6 +102,7 @@ suspend fun processLoadedChat(
val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0)
newItems.addAll(insertAt, chat.chatItems)
withContext(Dispatchers.Main) {
if (chatWasSwitched(chat, pagination, openAroundItemId)) return@withContext
chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits
chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems)
@@ -113,6 +122,7 @@ suspend fun processLoadedChat(
val indexToAddIsLast = indexToAdd == newItems.size
newItems.addAll(indexToAdd, chat.chatItems)
withContext(Dispatchers.Main) {
if (chatWasSwitched(chat, pagination, openAroundItemId)) return@withContext
chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits
chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems)
@@ -135,6 +145,7 @@ suspend fun processLoadedChat(
newSplits.add(splitIndex, chat.chatItems.last().id)
withContext(Dispatchers.Main) {
if (chatWasSwitched(chat, pagination, openAroundItemId)) return@withContext
chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits
unreadAfterItemId.value = chat.chatItems.last().id
@@ -158,6 +169,7 @@ suspend fun processLoadedChat(
removeDuplicates(newItems, chat)
newItems.addAll(chat.chatItems)
withContext(Dispatchers.Main) {
if (chatWasSwitched(chat, pagination, openAroundItemId)) return@withContext
chatsCtx.chatItems.replaceAll(newItems)
chatState.splits.value = newSplits
unreadAfterNewestLoaded.value = 0
@@ -0,0 +1,65 @@
package chat.simplex.app
import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.processLoadedChat
import chat.simplex.common.model.replaceAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.test.Test
import kotlin.test.assertEquals
class ChatItemsLoaderTest {
private fun groupChat(groupId: Long, itemIds: List<Long>): Chat =
Chat(
remoteHostId = null,
chatInfo = ChatInfo.Group(GroupInfo.sampleData.copy(groupId = groupId), groupChatScope = null),
chatItems = itemIds.map { ChatItem.getSampleData(it, CIDirection.GroupRcv(GroupMember.sampleData)) }
)
private suspend fun openChatWithItems(itemIds: List<Long>): Pair<ChatModel.ChatsContext, Chat> {
val chatsCtx = ChatModel.ChatsContext(null)
val opened = groupChat(groupId = 2, itemIds = itemIds)
withContext(Dispatchers.Main) {
chatsCtx.chatItems.replaceAll(opened.chatItems)
chatModel.chatId.value = opened.chatInfo.id
}
return chatsCtx to opened
}
private fun itemIds(chatsCtx: ChatModel.ChatsContext): List<Long> = chatsCtx.chatItems.value.map { it.id }
@Test
fun lastPageLoadedForAnotherChatIsNotAddedToOpenedChat() = runBlocking {
val (chatsCtx, _) = openChatWithItems(listOf(101, 102))
val anotherChat = groupChat(groupId = 1, itemIds = listOf(201, 202))
processLoadedChat(chatsCtx, anotherChat, NavigationInfo(), ChatPagination.Last(2), openAroundItemId = null)
assertEquals(listOf(101L, 102L), itemIds(chatsCtx))
}
@Test
fun lastPageLoadedForOpenedChatIsAdded() = runBlocking {
val (chatsCtx, _) = openChatWithItems(listOf(101, 102))
val sameChat = groupChat(groupId = 2, itemIds = listOf(103, 104))
processLoadedChat(chatsCtx, sameChat, NavigationInfo(), ChatPagination.Last(2), openAroundItemId = null)
assertEquals(listOf(101L, 102L, 103L, 104L), itemIds(chatsCtx))
}
@Test
fun beforePageLoadedForAnotherChatIsNotAddedToOpenedChat() = runBlocking {
val (chatsCtx, _) = openChatWithItems(listOf(101, 102))
val anotherChat = groupChat(groupId = 1, itemIds = listOf(201, 202))
processLoadedChat(chatsCtx, anotherChat, NavigationInfo(), ChatPagination.Before(101, 2), openAroundItemId = null)
assertEquals(listOf(101L, 102L), itemIds(chatsCtx))
}
@Test
fun aroundPageLoadedForAnotherChatIsNotAddedToOpenedChat() = runBlocking {
val (chatsCtx, _) = openChatWithItems(listOf(101, 102))
val anotherChat = groupChat(groupId = 1, itemIds = listOf(201, 202))
processLoadedChat(chatsCtx, anotherChat, NavigationInfo(), ChatPagination.Around(101, 2), openAroundItemId = null)
assertEquals(listOf(101L, 102L), itemIds(chatsCtx))
}
}
@@ -53,6 +53,7 @@ The web solved this by letting anyone build any experience on one open platform.
SimpleX Chat is the company that builds SimpleX Network, and now you can invest from $100 and get a stake in the company.
If you invest $500 or more, you receive [a public SimpleX name](./20260722-simplex-public-names.md) on SimpleX Network:
- for 7 years, if you invest by September 22,
- for 5 years for early bird investors,
- for 3 years after that.
@@ -0,0 +1,81 @@
---
layout: layouts/article.html
title: "Nick Rogers: Why I Backed SimpleX Chat in 2022, and Why I Doubled Down"
date: 2026-09-08
preview: "Blog post by Nick Rogers, early investor and advisor of SimpleX Chat, founder of Fieldwork, former CPO & CTO of Stream."
permalink: "/blog/20260908-nick-rogers-why-i-backed-simplex-chat.html"
---
# Why I Backed SimpleX Chat in 2022, and Why I Doubled Down
**Published:** Sep 08, 2026
*By [Nick Rogers](https://x.com/nickwd), Early Investor and Advisor, SimpleX Chat | Founder, Fieldwork | former CPO & CTO, Stream (Wagestream)*
---
When I first met Evgeny at Wagestream (now Stream) back in 2020, he was one of the most unique people I had ever encountered in technology.
He was fiercely, relentlessly honest. In a tech industry where corporate diplomacy usually trumps candour, that honesty struck me as genuine bravery. Evgeny was never afraid to speak truth to authority, and he held an uncompromisingly high bar for technical truth.
In late 2021, when he decided to step down as VP of Engineering to build a brand new messaging protocol from scratch, I took over as VP of Engineering.
I knew how massive the shoes were that I had to fill. But more than that, I knew that whatever Evgeny set out to build next, he would pursue with absolute conviction.
Most people in software already know Evgeny without realizing it. He is the creator of **Ajv** (Another JSON Schema Validator), one of the most widely used open source libraries in the JavaScript ecosystem, downloaded over a billion times each month by virtually every major technology company on earth. When someone has already built foundational infrastructure that the entire web secretly depends on, you take notice when they turn their attention to a new problem.
### The 2022 Bet: Backing a Protocol Before It Had Users
When Evgeny first pitched me on **SimpleX Chat**, his ambition was enormous: build an alternative to WhatsApp and Telegram that fundamentally eliminated user identifiers.
No phone numbers. No email addresses. No usernames. Not even a persistent public key or random user ID.
My first thought was that competing with WhatsApp was wildly ambitious. But as an engineering leader, I also knew there was a massive, structural gap in the market.
People assume end-to-end encryption solves privacy. It does not. WhatsApp, Telegram, Signal, and Matrix still rely on centralized account registries or user identifiers. Even when the message payload is encrypted, the platforms still collect, analyze, and classify the metadata: who you are, who you talk to, at what time, and from where. Between on-device classification, advertising networks, and platform-level censorship, modern messaging is fundamentally broken at the protocol layer.
When I wrote one of the first angel checks into SimpleX's SEIS round in early 2022 (at a $5M valuation), there were no mobile app store rankings. There was barely a user base. It was essentially a Haskell terminal prototype on GitHub and Evgeny's mathematical conviction that you could route messages over isolated, unidirectional queues without ever tracking who was talking to whom.
I invested because I believed in the architectural insight, and because I knew Evgeny was stubborn enough to work on this until it succeeded.
### Four Years Later: The Inflection Point
Over the last four years, as an investor and advisor, I have watched that initial command-line prototype grow into a global network:
* **Almost 500,000 Monthly Active Users** (up from zero when I invested).
* **3 Million+ App Downloads** and **over 20 Million messages per day**.
* **Zero paid marketing spend.**
SimpleX has reached this size because it started with the people who need uncompromising privacy the most: journalists, whistleblowers, developers, cryptographers, and sovereign communities.
More importantly, it has evolved from a 1:1 private messenger into a platform for **sovereign publishing and broadcast channels**. Platforms like Telegram banned tens of millions of channels in 2025 alone, proving to creators and communities that they do not own their audience on centralized platforms. On SimpleX channels, publishers cannot be revoked, subscribers cannot be tracked, and content cannot be taken down centrally.
### Why the Commercial Model Works: Scale, Low ARPU, and Organic Demand
A common criticism of privacy tools is that they make great philosophical statements but terrible businesses.
I take the opposite view for two reasons:
1. **Unsolicited willingness to pay:** Before SimpleX even introduced paid commercial tiers, users voluntarily gave over **$650,000 in unsolicited donations**. When a community gives you hundreds of thousands of dollars purely to support the software, you have a level of organic product love and willingness to pay that most consumer startups spend tens of millions of venture dollars trying to manufacture.
2. **Viral dynamics and low ARPU:** Messaging apps are inherently viral networks. Once a protocol achieves scale, building a high-margin, profitable business is straightforward even at very low average revenue per user (ARPU).
SimpleX is not monetizing through surveillance advertising. It is monetizing through core infrastructure utilities:
* **SimpleX Domains (`.simplex` public names launching Dec 12):** Memorable names for public channels and contact addresses, providing recurring utility revenue.
* **Paid Supporter Badges and Dedicated Relays:** High-throughput infrastructure for power users and enterprise teams.
* **Enterprise Messaging:** Allowing businesses to talk to customers via their websites, without any privacy compromises of existing solutions like Intercom or Drift (especially important for sectors that require anonymity by law).
### Why I Doubled Down, and Why You Should Join the Wefunder Round
Messaging is the most fundamental layer of human communication on the internet. It should look more like email: an open, decentralized protocol that nobody owns, rather than a walled garden controlled by Big Tech.
When Evgeny opened an insider round in mid-2026, I immediately wrote another check. And when he decided to take this round to **Wefunder** ([wefunder.com/simplexchat](https://wefunder.com/simplexchat?utm_source=nr_post)), I was proud to represent the community of investors backing him.
In startup investing, you back two things: **the founder and the TAM**.
The market for private, sovereign communications is enormous. And Evgeny is the rarest kind of founder: technically brilliant, relentlessly honest, and stubborn enough to see an audacious mission through to the end.
If you believe that the future of communication should belong to individuals rather than centralized platforms, I encourage you to join us as an investor.
👉 **Invest in SimpleX Chat on Wefunder:** [wefunder.com/simplexchat](https://wefunder.com/simplexchat?utm_source=nr_post)
*(Note: Anyone investing $500 or more before September 22 secures their `.simplex` public name for 7 years prior to the public launch.)*
+81
View File
@@ -0,0 +1,81 @@
(function () {
var LANDINGS = [
{ path: '/subscribe/thankyou', status: 'subscribed' },
{ path: '/subscribe/confirmed', status: 'confirmed' }
];
var MESSAGES = {
'mc:subscribed': 'subscribed',
'mc:confirmed': 'confirmed',
'mc:failed': 'failed'
};
var TIMEOUT = 12000;
var REDIRECT_GRACE = 5000;
function init() {
var forms = document.querySelectorAll('form[action*="list-manage.com"]');
if (!forms.length) return;
var frame = document.createElement('iframe');
frame.name = 'mc-target';
frame.title = 'Subscription result';
frame.className = 'mc-frame';
(document.querySelector('.mc-frame-host') || document.body).appendChild(frame);
var pending = false;
var timer = null;
function done(status) {
if (!pending) return;
pending = false;
clearTimeout(timer);
document.dispatchEvent(new CustomEvent('mc:result', {
detail: { ok: status !== 'failed', status: status }
}));
}
function landedStatus() {
var path;
try {
path = frame.contentWindow.location.pathname;
} catch (e) {
return null;
}
for (var i = 0; i < LANDINGS.length; i++) {
if (path.indexOf(LANDINGS[i].path) === 0) return LANDINGS[i].status;
}
return null;
}
window.addEventListener('message', function (e) {
if (!pending || e.source !== frame.contentWindow) return;
var status = MESSAGES[e.data];
if (status) done(status);
});
frame.addEventListener('load', function () {
if (!pending) return;
var status = landedStatus();
if (status) {
done(status);
return;
}
clearTimeout(timer);
timer = setTimeout(function () { done('failed'); }, REDIRECT_GRACE);
});
forms.forEach(function (form) {
form.target = 'mc-target';
form.addEventListener('submit', function () {
pending = true;
clearTimeout(timer);
timer = setTimeout(function () { done('failed'); }, TIMEOUT);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+3 -1
View File
@@ -148,7 +148,7 @@
</svg>
</button>
{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('credits' not in page.url) and ('file' not in page.url) and ('links' not in page.url) and ('news' not in page.url) and ('crowdfunding' not in page.url) and ('livestream' not in page.url) %}
{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('credits' not in page.url) and ('file' not in page.url) and ('links' not in page.url) and ('news' not in page.url) and ('crowdfunding' not in page.url) and ('livestream' not in page.url) and ('subscribe' not in page.url) %}
<div class="nav-link flag-container">
<a href="javascript:void(0);">
{% for language in languages.languages %}
@@ -210,6 +210,8 @@
</div>
<script>{% include "utm.js" %}</script>
<script>
// switch theme
const sunIcon = document.querySelector('.sun');
+60
View File
@@ -0,0 +1,60 @@
(function () {
var VALID = /^[\w.-]{1,40}$/;
var KEYS = ['utm_source', 'utm_campaign'];
function fromUrl(name) {
var hash = new URLSearchParams(location.hash.replace(/^#\??/, '')).get(name);
var query = new URLSearchParams(location.search).get(name);
var value = hash != null ? hash : query;
return value && VALID.test(value) ? value : null;
}
function remember(name, value) {
try {
if (value) sessionStorage.setItem(name, value);
return value || sessionStorage.getItem(name);
} catch (e) {
return value;
}
}
var utm = {};
KEYS.forEach(function (name) {
var value = remember(name, fromUrl(name));
if (value && VALID.test(value)) utm[name] = value;
});
if (!utm.utm_source && !utm.utm_campaign) return;
function fill(name, value) {
if (!value) return;
document.querySelectorAll('input[name="' + name + '"]').forEach(function (input) {
input.value = value;
});
}
function withUtm(href) {
var url = new URL(href, location.href);
KEYS.forEach(function (name) {
if (utm[name]) url.searchParams.set(name, utm[name]);
});
return url.toString();
}
function apply() {
fill('SOURCE', utm.utm_source);
fill('CAMPAIGN', utm.utm_campaign);
if (/^\/blog(\/|$)/.test(location.pathname)) return;
document.querySelectorAll('a[href^="https://wefunder.com/"]').forEach(function (link) {
link.href = withUtm(link.getAttribute('href'));
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', apply);
} else {
apply();
}
})();
File diff suppressed because it is too large Load Diff
-125
View File
@@ -1,125 +0,0 @@
---
layout: layouts/page.html
title: "Get a Stake in SimpleX Chat"
description: "SimpleX Chat equity crowdfunding on Wefunder - invest in the first messaging network without any user IDs."
permalink: "/crowdfunding/index.html"
---
<style>
.cf-actions { text-align: center; margin: 32px 0 8px 0; clear: both; }
.cf-btn { display: inline-block; background: #0053D0; color: #ffffff !important; border: none; border-radius: 9999px; padding: 12px 40px; font-weight: 500; font-size: 1rem; cursor: pointer; text-decoration: none !important; }
.dark .cf-btn { background: #70F0F9; color: #000000 !important; }
.cf-ask { display: block; margin-top: 14px; }
.cf-form { display: flex; gap: 8px; max-width: 30rem; flex-wrap: wrap; margin-top: 12px; }
.cf-form input[type=email] { flex: 1; min-width: 220px; border: 1px solid #4f4f4f; border-radius: 9999px; padding: 10px 18px; background: transparent; color: inherit; }
#article .ls-banner { display: block; clear: both; margin: 32px 0; padding: 20px 24px; border-radius: 10px; background: #DBEEFF; text-decoration: none; }
.dark #article .ls-banner { background: #1B325C; }
#article .ls-banner-label { display: block; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #0053D0; }
.dark #article .ls-banner-label { color: #70F0F9; }
#article .ls-banner-title { display: block; margin-top: 8px; font-size: 1.35rem; font-weight: 600; line-height: 1.25; color: #023789; }
.dark #article .ls-banner-title { color: #ffffff; }
#article .ls-banner-date { display: block; margin-top: 6px; color: #3F484B; }
.dark #article .ls-banner-date { color: #ffffff; }
#article .ls-banner-cta { display: inline-block; margin-top: 16px; font-weight: 500; color: #0053D0; }
.dark #article .ls-banner-cta { color: #70F0F9; }
</style>
# Get a Stake in SimpleX Chat
<img src="/img/crowdfunding/crowdfunding_1.jpg" width="50%" class="float-to-right" style="border-radius: 10px;">
SimpleX Chat &mdash; the company that builds the first and the only messaging network without any user identifiers &mdash; is raising its community round. By investing, you can benefit from the company growth, and help us build the future of private and secure communications. [Learn more and invest on Wefunder](https://wefunder.com/simplex.chat?utm_source=website).
<a class="ls-banner" href="/livestream/">
<span class="ls-banner-label">Live event</span>
<span class="ls-banner-title">SimpleX Chat: Foundation for the Future</span>
<span class="ls-banner-date">Livestream and Q&amp;A about SimpleX Chat roadmap and crowdfunding. Tuesday, September 15, 2026 at 5:00 PM UTC.</span>
<span class="ls-banner-cta">Open event page</span>
</a>
## Every other network can identify users
For 150 years, all networks required user identifiers &mdash; phone numbers, usernames, random IDs or public keys. To be reachable, you had to be identifiable. User identification gives networks power over users: to surveil their private life, to sell their data, and to revoke access. Even encrypted platforms see who you talk to, when, and where &mdash; and pseudonyms don't help, because your contacts are a fingerprint.
Identification also gives platforms the power to censor public speech without due process. Telegram blocked 44 million groups and channels in 2025 alone. Publishers and content creators build audiences on borrowed infrastructure &mdash; one policy change, and the audience that took years to build can be gone.
The root cause of these problems is user identifiers. The solution is not better policies or stronger encryption with the same architecture &mdash; it is a different architecture, without any user IDs.
## The first network without any user identifiers
<img src="/img/crowdfunding/no-user-ids.webp" width="50%" class="float-to-right" style="border-radius: 10px;">
We invented and built SimpleX Network. It uses no phone numbers, emails, or user accounts &mdash; every conversation has its own network address, removing the need for any user IDs. There is no user data on the servers that could be breached or sold &mdash; all user data exists only on user devices. The network runs on multiple independent operators &mdash; SimpleX Chat and Flux are preconfigured in the app, and anyone can run their own servers.
SimpleX Chat also supports public groups and channels, where owners fully control them, and nobody can take their work, because only the users hold the keys. SimpleX software is open-source and based on open protocols, audited by Trail of Bits in 2022, 2024, and 2026 (to be published).
## 480,000+ users joined on their own
<img src="/img/crowdfunding/crowdfunding_2.jpg" width="50%" class="float-to-right" style="border-radius: 10px;">
Over 480,000 people use SimpleX Chat every month &mdash; more than doubling every year &mdash; and all of them found it without any paid marketing. 3 million people have downloaded the app, and users send 20 million messages a day via pre-configured servers.
Users have donated over $650,000, paying for something they could use for free. All of it was built on a total investment of just $1.7M by 5 people team over 4.5 years.
## A network others build on
<img src="/img/crowdfunding/crowdfunding_3.jpg" width="50%" class="float-to-right" style="border-radius: 10px;">
In addition to people who use SimpleX Chat for private messaging, four other kinds of participants came to the network: creators with thousands of public groups and channels, businesses supporting customers over SimpleX, developers building on the open protocols (19K+ GitHub stars), and ~1,000 servers run by volunteers, plus Flux and StormyCloud. Each group makes the network more valuable to the rest, driving organic growth.
Independent developers created moderation and AI bots, Telegram bridges, and a public server registry. Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat. And the [SimpleX Network Consortium](https://simplexnetwork.org/consortium.html) &mdash; an agreement between a non-profit foundation and SimpleX Chat &mdash; prevents any single company from controlling the network.
## Why SimpleX cannot be copied
<img src="/img/crowdfunding/comparison.webp" width="50%" class="float-to-right" style="border-radius: 10px;">
No other communication system combines scalable one-to-many delivery, sovereign ownership, infrastructure independence, and participation privacy &mdash; and removing user identifiers from an existing network is hard for three reasons:
- **Technically**: other networks rely on user IDs to route messages &mdash; they would have to rebuild from scratch.
- **Economically**: large platforms monetize user identifiers &mdash; removing IDs would destroy their revenue model.
- **Cold start**: a newcomer would need users, creators, businesses, developers, and servers, all at the same time &mdash; while competing with the SimpleX Network.
## Why now: three trends that may grow SimpleX
<img src="/img/crowdfunding/why-now.png" width="50%" class="float-to-right" style="border-radius: 10px;">
- **Growing surveillance of private life**: big centralized platforms are keen to scan private messages to train their AI models, supported by proposed laws. More people would move to private messaging that keeps no record of who they talk to.
- **Accelerating deplatforming**: more and more creators are removed from centralized platforms, without any due process. An audience built over years can disappear with one policy change.
- **AI agents acting for people**: if a platform controls an agent's identity, the platform can shut the agent down or turn it against the person it works for. User-controlled IDs are a basic security for the agentic Internet, not a preference.
All three trends increase the demand for identity-free messaging. None of the existing networks can provide it.
## Revenue plan: free for users, channels & businesses pay
<img src="/img/crowdfunding/crowdfunding_4.jpg" width="50%" class="float-to-right" style="border-radius: 10px;">
Private messaging should remain free for the users. Instead, we plan to earn from the infrastructure and services that channels and businesses need:
- **SimpleX public names** &mdash; globally unique names for a public channel or business, paid yearly and controlled by the owner's key, so no one can seize them. If you invest $500 or more, you would receive a name as a perk for 5 years during early bird, and for 3 years after that.
- **Business messaging** &mdash; a web widget that adds encrypted chat to any website with one line of code, built for privacy-first businesses and sectors where anonymity is required.
- **Paid servers for big channels** &mdash; channels that have grown beyond the free tier pay for the servers that deliver their messages, with server operators earning most of the revenue.
Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms, on Wefunder.
## Get a stake
We are building a network that people own. Businesses, creators, and publishers keep the audiences and communities they build, and no company can take them away or unilaterally shut them down. By investing, you benefit from the company growth, and become part of building that future.
<div class="cf-actions">
<a class="cf-btn" href="https://wefunder.com/simplex.chat?utm_source=website" target="_blank" rel="noopener">Learn more on Wefunder</a>
<a class="cf-ask" href="https://smp11.simplex.im/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo" target="_blank" rel="noopener">or ask questions via SimpleX Chat</a>
</div>
<!-- Hidden until the Mailchimp flow is wired: replace MAILCHIMP_U / MAILCHIMP_ID with the audience values from the embed code, then uncomment.
## Get more information
Leave your email, and we will send you more information about investing in SimpleX Chat.
<form class="cf-form" action="https://simplex.us1.list-manage.com/subscribe/post?u=MAILCHIMP_U&amp;id=MAILCHIMP_ID" method="post" target="_blank">
<input type="email" name="EMAIL" placeholder="you@example.com" required>
<div style="position:absolute;left:-5000px" aria-hidden="true"><input type="text" name="b_MAILCHIMP_U_MAILCHIMP_ID" tabindex="-1" value=""></div>
<button class="cf-btn" type="submit">Send me more information</button>
</form>
-->
+42
View File
@@ -281,6 +281,7 @@ main .section-bg {
.register-card input[type="submit"] {
height: 52px;
min-width: 164px;
padding: 0 34px;
border: none;
border-radius: 9999px;
@@ -297,6 +298,16 @@ main .section-bg {
color: #000000;
}
.register-card .register-note {
margin-top: 10px;
font-size: 16px;
color: #6b7478;
}
.dark .register-card .register-note {
color: #9fb2c9;
}
.register-card .channel-link {
display: inline-flex;
align-items: center;
@@ -344,3 +355,34 @@ main .section-bg {
.dark .register-card .close-register svg {
fill: #ffffff;
}
.mc-frame {
position: absolute;
left: -5000px;
width: 0;
height: 0;
border: 0;
}
.mc-frame-card {
max-width: 640px;
padding: 44px 0 0;
overflow: hidden;
background: #ffffff;
}
.dark .mc-frame-card {
background: #ffffff;
}
.mc-frame-card .close-register svg,
.dark .mc-frame-card .close-register svg {
fill: #023789;
}
#subscribe-failed.open .mc-frame {
position: static;
display: block;
width: 100%;
height: min(520px, calc(100svh - 140px));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

+8 -11
View File
@@ -49,15 +49,6 @@ function startCountdown() {
setInterval(tick, 1000);
}
function setSignupSource() {
const field = document.querySelector('input[name="SOURCE"]');
if (!field) return;
const inHash = new URLSearchParams(location.hash.replace(/^#\??/, '')).get('utm_source');
const source = inHash ?? new URLSearchParams(location.search).get('utm_source');
if (source && /^[\w.-]{1,40}$/.test(source)) field.value = source;
}
function setupRegisterOverlay() {
const overlay = document.getElementById('register');
const openBtn = document.querySelector('.register-btn');
@@ -79,7 +70,14 @@ function setupRegisterOverlay() {
}
openBtn.addEventListener('click', openOverlay);
form.addEventListener('submit', closeOverlay);
form.addEventListener('submit', () => {
const submit = form.querySelector('[type="submit"]');
if (!submit) return;
setTimeout(() => {
submit.disabled = true;
submit.value = 'Submitting...';
}, 0);
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay || e.target.closest('.close-register')) closeOverlay();
});
@@ -107,6 +105,5 @@ function trackNavColor() {
showLocalTime();
startCountdown();
setSignupSource();
setupRegisterOverlay();
trackNavColor();
+78 -1
View File
@@ -94,9 +94,11 @@ templateEngineOverride: njk
<h2 id="register-title">Register for event updates and Q&amp;A</h2>
<p>We will send you the updates about the event, and you will be able to ask any questions.</p>
<form action="https://chat.us2.list-manage.com/subscribe/post?u=ddd892b258ae36e5438e6d4e1&amp;id=6f73f92ffa&amp;f_id=006bf8e3f0" method="post" target="_blank">
<form action="https://chat.us2.list-manage.com/subscribe/post?u=ddd892b258ae36e5438e6d4e1&amp;id=6f73f92ffa&amp;f_id=001df8e3f0&amp;legacy_form=1" method="post" target="_blank">
<input type="email" name="EMAIL" placeholder="Enter your email address" aria-label="Email address" required>
<input type="hidden" name="PAGE" value="livestream">
<input type="hidden" name="SOURCE" value="livestream_page">
<input type="hidden" name="CAMPAIGN" value="">
<span aria-hidden="true" class="bot-field">
<input type="text" name="b_ddd892b258ae36e5438e6d4e1_6f73f92ffa" tabindex="-1" value="">
</span>
@@ -105,6 +107,42 @@ templateEngineOverride: njk
<a class="channel-link" href="https://simplex.chat/crowdfunding-news/" target="_blank" rel="noopener">Or join our SimpleX Crowdfunding News channel<svg viewBox="0 0 100 100" aria-hidden="true"><path d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path><polygon points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>
<p class="register-note">We use Mailchimp to deliver updates via email</p>
<button type="button" class="close-register" aria-label="Close">
<svg viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
</svg>
</button>
</div>
</div>
<div id="subscribed" class="register-overlay">
<div class="register-card" role="dialog" aria-labelledby="subscribed-title">
<h2 id="subscribed-title">Thank you for subscribing</h2>
<p>Please confirm your subscription by clicking the link in the email.</p>
<button type="button" class="close-register" aria-label="Close">
<svg viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
</svg>
</button>
</div>
</div>
<div id="already-subscribed" class="register-overlay">
<div class="register-card" role="dialog" aria-labelledby="already-subscribed-title">
<h2 id="already-subscribed-title">You are already subscribed</h2>
<p>This address is on our list — you will receive the updates about the event.</p>
<button type="button" class="close-register" aria-label="Close">
<svg viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
</svg>
</button>
</div>
</div>
<div id="subscribe-failed" class="register-overlay">
<div class="register-card mc-frame-card mc-frame-host" role="dialog" aria-label="Subscription result">
<button type="button" class="close-register" aria-label="Close">
<svg viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7973 11.5525L7.59762 6.49833L12.7947 1.44675C13.055 1.19371 13.0658 0.771991 12.8188 0.505331C12.5718 0.238674 12.1602 0.227644 11.8999 0.480681L6.65343 5.58028L1.09979 0.182228C0.839522 -0.070157 0.427909 -0.059127 0.18094 0.207531C-0.0660305 0.474191 -0.0552645 0.895911 0.205003 1.14894L5.70862 6.49833L0.20247 11.851C-0.0577975 12.104 -0.0685635 12.5257 0.178407 12.7924C0.306324 12.9306 0.477936 13 0.650181 13C0.811033 13 0.971873 12.9397 1.09726 12.817L6.65343 7.41639L11.9025 12.5186C12.0285 12.6406 12.1893 12.7015 12.3495 12.7015C12.5218 12.7015 12.6934 12.6321 12.8213 12.4939C13.0689 12.2273 13.0582 11.8062 12.7973 11.5525Z"/>
@@ -114,6 +152,45 @@ templateEngineOverride: njk
</div>
<script src="/js/livestream.js"></script>
<script>{% include "mc-submit.js" %}</script>
<script>
(function livestreamSubscribe() {
var register = document.getElementById('register');
var byStatus = {
subscribed: document.getElementById('subscribed'),
confirmed: document.getElementById('already-subscribed'),
failed: document.getElementById('subscribe-failed')
};
var results = Object.keys(byStatus).map(function (k) { return byStatus[k]; });
function close(overlay) {
overlay.classList.remove('open');
document.documentElement.classList.remove('lock-scroll');
}
document.addEventListener('mc:result', function (e) {
var submit = register.querySelector('[type="submit"]');
if (submit) {
submit.disabled = false;
submit.value = 'Register';
}
register.classList.remove('open');
(byStatus[e.detail.status] || byStatus.failed).classList.add('open');
document.documentElement.classList.add('lock-scroll');
});
results.forEach(function (overlay) {
overlay.addEventListener('click', function (e) {
if (e.target === overlay || e.target.closest('.close-register')) close(overlay);
});
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') results.forEach(close);
});
})();
</script>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
---
title: "Subscription confirmed - SimpleX Chat"
description: "Your subscription to SimpleX Chat updates is confirmed."
permalink: /subscribe/confirmed/
templateEngineOverride: njk
---
<!DOCTYPE html>
<html class="bg-[#F3FAFF] dark:bg-[#000832]" lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
{% include "dark-mode.html" %}
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ title }}</title>
<meta name="description" content="{{ description }}"/>
<meta name="robots" content="noindex">
<meta name="Content-Type" content="text/html;charset=utf-8"/>
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon.ico"/>
<meta name="theme-color" content="#F3FAFF">
<link href="/css/tailwind.css" rel="stylesheet"/>
<link href="/css/style.css" rel="stylesheet"/>
<link rel="stylesheet" href="/css/design3-nav.css">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100svh;
}
.thankyou {
flex: 1;
padding: 180px 24px 120px;
max-width: 760px;
margin: 0 auto;
text-align: center;
}
.thankyou h1 {
font-family: "GT-Walsheim", sans-serif;
font-weight: 400;
font-size: clamp(30px, 4vw, 44px);
line-height: 1.15;
letter-spacing: -0.025em;
color: #023789;
}
.dark .thankyou h1 { color: #ffffff; }
.thankyou p {
margin-top: 20px;
font-family: "Manrope", sans-serif;
font-weight: 300;
font-size: clamp(16px, 1.4vw, 19px);
line-height: 1.6;
color: #3f484b;
}
.dark .thankyou p { color: #dfeaff; }
</style>
<script>
if (window.parent !== window) window.parent.postMessage('mc:confirmed', '*');
</script>
</head>
<body class="bg-[#F3FAFF] dark:bg-[#000832]">
{% include "navbar.html" %}
<main class="thankyou">
<h1>Your subscription<br>is confirmed</h1>
<p>Thank you. We will send SimpleX Chat updates to this address.</p>
</main>
{% include "footer.html" %}
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
---
title: "Thank you for subscribing - SimpleX Chat"
description: "Please confirm your subscription to SimpleX Chat updates."
permalink: /subscribe/thankyou/
templateEngineOverride: njk
---
<!DOCTYPE html>
<html class="bg-[#F3FAFF] dark:bg-[#000832]" lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
{% include "dark-mode.html" %}
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ title }}</title>
<meta name="description" content="{{ description }}"/>
<meta name="robots" content="noindex">
<meta name="Content-Type" content="text/html;charset=utf-8"/>
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon.ico"/>
<meta name="theme-color" content="#F3FAFF">
<link href="/css/tailwind.css" rel="stylesheet"/>
<link href="/css/style.css" rel="stylesheet"/>
<link rel="stylesheet" href="/css/design3-nav.css">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100svh;
}
.thankyou {
flex: 1;
padding: 180px 24px 120px;
max-width: 760px;
margin: 0 auto;
text-align: center;
}
.thankyou h1 {
font-family: "GT-Walsheim", sans-serif;
font-weight: 400;
font-size: clamp(30px, 4vw, 44px);
line-height: 1.15;
letter-spacing: -0.025em;
color: #023789;
}
.dark .thankyou h1 { color: #ffffff; }
.thankyou p {
margin-top: 20px;
font-family: "Manrope", sans-serif;
font-weight: 300;
font-size: clamp(16px, 1.4vw, 19px);
line-height: 1.6;
color: #3f484b;
}
.dark .thankyou p { color: #dfeaff; }
</style>
<script>
if (window.parent !== window) window.parent.postMessage('mc:subscribed', '*');
</script>
</head>
<body class="bg-[#F3FAFF] dark:bg-[#000832]">
{% include "navbar.html" %}
<main class="thankyou">
<h1>Thank you for subscribing<br>to SimpleX Chat updates</h1>
<p>Please confirm your subscription by clicking the link in the email we just sent you.</p>
</main>
{% include "footer.html" %}
</body>
</html>