Blip safety and moderation
Blip is a messaging app operated by one person, Andrew Bui, in California. People can send each other text, photos and videos, and they can see each other typing before a message is sent. That means people can also misuse it, so Blip has to have a way for you to report what you see, a way to stop someone reaching you, and a person who reads the reports.
This page explains all three, and it explains the limits. It is deliberately specific: a safety policy that describes a process nobody performs is worse than one that admits what it is.
The short version
- You can report any message somebody else sent, in any conversation.
- You can report a person you are in a one-to-one conversation with.
- Reporting also blocks. You do not have to do both.
- A report takes a copy of the reported message as the server saw it, so it still exists if the sender deletes the message afterwards.
- The reports go to one person, who reads them and can delete the message and disable the account behind it.
- Nobody reads your conversations. Moderation sees what was reported and nothing else. There is no console for browsing messages, and unsent typing is never captured at all.
- There is no automated scanning of messages, no proactive detection, and no 24/7 team. One operator, one queue.
- Child sexual abuse material means the account is terminated and the case is reported to NCMEC. There is no second chance and no warning.
What you can do, and where it is
| You want to | Where it is |
|---|---|
| Report a message | Press and hold the message, then Report message |
| Report a person | Open the conversation, tap the menu in the header, then Report user |
| Report a person, from the inbox | Swipe a one-to-one row and tap the flag, or press and hold the row and choose Report |
| Block someone | The conversation's header menu, then Block user -- or press and hold their row in the inbox, then Block |
| Unblock someone | Profile then Blocked contacts, then the icon on their row |
| Email a person about it | Profile then About then Report a safety concern (safety@tryblip.app) |
Reporting a message
Press and hold any message somebody else sent. The card that appears offers Report message, and asks you to confirm with Report and block. In a one-to-one conversation it tells you the person will be blocked. In a group it tells you the truth about a group instead: they will not be able to message you directly, but they can still post in the group, because one member silencing another for everybody is not something a private setting should do.
You stay in the conversation afterwards. You reported a message, not a person, and being thrown out of a group because one member misbehaved would be its own bug. The card turns into Report sent so you know it worked -- nothing else on the screen changes when a report lands, by design.
This is the only reporting route inside a group, and it is the one that matters most, because it is the only one that hands the operator the actual content to act on.
Reporting a person
In a one-to-one conversation: tap the menu in the conversation header and choose Report user. From the Messages tab: swipe the row and tap the flag, or press and hold the row and choose Report. Reporting a person closes the conversation and takes you back to the inbox -- you have just said you do not want to hear from them.
A group has no "report user" row. A group conversation has no single other person, so reporting inside a group goes through the message. (Code comments in ConversationSettingsSheet.swift and MessagesHoldOverlay.swift say the member list offers this route; it does not. The member list offers the group's creator a way to remove a member, which is a different thing.)
Blocking
Blocking is enforced on the server, not hidden in the app. When you block someone:
- their messages to you in your one-to-one conversation are refused by Security Rules, not filtered after the fact;
- the live relay stops in both directions, and any draft already in flight is cleared (
syncBlockedLiveTyping); - they stop being able to write the preview row that would otherwise sit at the top of your inbox;
- their notifications stop.
Three things blocking does not do, stated because you should not have to guess:
- A group you both belong to is exempt. Their messages still appear there -- though their notifications still do not reach you, because the notification fan-out checks your block list for every conversation. Removing a member from a group is the group admin's job.
- It is not retroactive. Messages already in the conversation stay where they are.
- It works one way for messages, and both ways for live typing. It stops them messaging you and notifying you, and you can still message them. But the relay, the presence balloons and the inbox row a reaction would write all belong to the conversation rather than to one person, so they go dark for both of you while the block stands --
blockedBetweenis asked symmetrically, andmessageEvents.tssays so in as many words. It is still your setting, and your block list is private to you.
Unblocking
Profile, then Blocked contacts, then the icon on their row. It confirms first, and tells you what it means: they will be able to message you again.
Emailing a person
Profile, then About, then Report a safety concern, which opens a mail composer to safety@tryblip.app. This is the route for anything the in-app buttons do not cover -- threats, someone contacting you through a second account, a situation that needs explaining. There is also Contact support (support@tryblip.app) for everything that is not a safety matter.
No response time is promised anywhere in the app, and none is promised here. See The honest limits.
What a report captures
A report is written by a Cloud Function, submitReport (firebase/functions/src/reports.ts), and not by the app. That matters: the app is trusted only to say which message it is pointing at. The function re-reads that message with server privileges and snapshots what it actually says, so a report cannot describe text nobody sent.
| Captured | Detail |
|---|---|
| The message text | captureText copies the body as the server reads it. Capped at 2000 code points, the same cap Security Rules put on a message in the first place. Over that it is truncated, not dropped, and capturedTextTruncated is set to true so a reader knows they are looking at the start of something longer. It truncates by code point rather than by UTF-16 unit, so it cannot cut an emoji in half |
| Media | The storage path of an attached photo or video, not a second copy of the file |
| Whether it was already retracted | capturedWasRetracted. A report is still filed for a message the sender has pulled: the body has gone, but the fact they pulled it is part of the picture, and it explains an empty snapshot |
| Who sent it | The message's own senderId, as stored -- not whoever the app claimed. If the two disagree, both are kept, because a disagreement is worth seeing |
| Who reported it | The reporter's account id, taken from their authenticated session |
| Where | The conversation id, and whether it is a one-to-one or a group |
| Who they were at the time | The reported account's display name and username when the report was filed, since anyone can rename before it is read |
| When | The time the message was sent, and the time the report was filed |
| A reason | An optional free-text field, capped at 500 characters. The current app sends none -- there is no reason box in the UI yet |
A report about a person rather than a message captures no message at all: there is nothing for the server to verify, so there is nothing stored.
Two things are deliberately absent. The captured text is never written to the logs -- the log line for a filed report carries ids only, for the same reason the sign-up function never logs a date of birth. And capturedText and reason are excluded from Firestore's automatic indexes (firebase/firestore.indexes.json), so private content held for a second purpose does not get copied into a third system to make it searchable.
Who sees a report, and how
The report lands in the reports collection in Firestore. No app can read it. Security Rules deny read and write on reports to every client, including the person who filed it (firebase/firestore.rules, match /reports/{reportId} { allow read, write: if false; }), and there are rules tests covering exactly that (firebase/tests/firestore.test.js). A readable report would be a way to recover the text of a message the sender had since deleted, so the reporter cannot read their own.
The operator reads the queue in the Firebase console: reports, filtered to status == "open", oldest first. The composite index for that query is declared in firebase/firestore.indexes.json (status ascending, createdAt ascending), so it is deployed with the rest of the project's indexes. That console query is the whole moderation tool. There is no dashboard, and nothing else was built.
Nothing notifies him. This is the part most likely to be assumed and is not true today: no email is sent, no push, no alert. A filed report writes a report filed log entry and sits in Firestore until somebody looks. Turning that into a notification is the first item on the operator's own checklist, which is kept out of this page, and until it is done the queue is worked from a recurring reminder.
Acting on a report
One function, resolveReport, does everything an operator can do, and every action has to name a report id -- so there is no way to disable an account without a row on the record saying why. It can:
- delete the reported message. A hard delete: the row leaves the transcript for everyone, and the existing
onMessageDeletedtrigger removes the stored photo or video, the notification rows that quoted it, and the inbox preview if it pointed there; - disable the account. Sign-in stops and refresh tokens are revoked. Be clear about what that is and is not: it deletes nothing -- profile, username, messages and conversations all stay -- and a token already on the device keeps working until it expires, up to an hour. Nothing can shorten that;
- close the report, as
actionedordismissed, with a note and a record of what was actually done.
It is gated on a moderator claim, and that claim is deliberately not the admin claim the Security Rules read. The rules use admin to mean "written by the server, never by a client"; giving a real person's account that claim would quietly make the comment false and hand a signed-in phone write access through every server-only rule in the file, other people's private profiles included. A separate claim keeps the operator powerful in exactly one place: this one function. It is invoked from a terminal, not from the app -- nothing in Blip calls it.
Where the privacy line is
Blip's whole posture is that it collects as little as it can. Moderation is the one place where a person looks at private content, so the line is stated as a principle rather than left to trust:
Moderation sees what was reported. Never a conversation at large, and never an unsent draft.
Concretely:
- A report contains one message. Not the thread it came from, not the messages before or after it, not anything else the two people have said to each other.
- Unsent typing is never captured. Live drafts exist in the Realtime Database only while you are connected and are removed when you send, clear the field, background the app or lose connection. They are never copied into Firestore, never put in a notification, never logged -- and a report has no field for one. There is no way to report a draft, because there is nothing stored to report.
- There is no admin console for browsing messages. No search, no bulk export, no "open this user's conversations" screen. None of it was built, in the app or out of it.
- Nothing sees messages automatically. No scanner, no classifier, no keyword filter. A message is looked at by a person only if somebody reported it.
Being exact about what this is: the limit is the tooling and the practice, not a technical wall. Firebase data is held in a Google Cloud project, and a project owner can open the console. What can honestly be said is that no product surface, no function and no script in this repository reads, searches, exports or retains a conversation for moderation purposes, and that the only content a report puts in front of a human is the message that was reported.
What would have to change for that to stop being true
Stated so that a future change is visible as a change, rather than a drift:
- a Security Rules edit letting any client read
reports; - granting a person the
adminclaim the rules treat as "the server", instead ofmoderator; - a report that captured surrounding messages, a whole thread, or a media file rather than a path;
- any code that writes a live draft to Firestore, to a log, or into a report;
- an export or search function over
conversations, or an operator UI that lists them; - automated scanning of message content, which would mean every message being read by something.
Any one of those makes a sentence on this page false, and the page has to change with it.
What stops the report button being abused
submitReport already enforces all of this, server-side:
- A budget: ten reports per hour, per person. Far above real use, far below what a script does. The reason is not storage. It is that the queue is one person's attention, and an unbounded report button is an unbounded call on it.
- One open report per subject. Reporting the same message twice is refused while the first report is still open, and refused before it spends any of the hourly budget. A message report is keyed to the message, so two abusive messages from the same person are two reports, as they should be. A person report is keyed to the account. Once a report is resolved, the same subject can be reported again.
- You cannot report yourself, by account id or by pointing at your own message under someone else's name.
- You can only report from a conversation you are in, checked against the conversation's member list on the server.
- The message has to exist, and the account in a person report has to exist.
- App Check is enforced, so the endpoint answers the real app rather than a script holding a stolen token.
All of it is covered by tests against the emulators (firebase/tests/reports.callable.test.js, run by npm run test:functions from firebase/tests).
Child sexual abuse material
Zero tolerance. Plainly:
- Any account used to share or solicit child sexual abuse material is terminated, immediately and permanently. Not warned, not suspended pending appeal.
- The case is reported to the National Center for Missing and Exploited Children (NCMEC), through the CyberTipline. US law requires a provider who becomes aware of apparent child sexual abuse material to report it, and requires the reported material to be preserved for 90 days after the report is made.
- The order of operations matters and is written down here because the tooling makes it easy to get wrong: report and preserve first, delete second.
resolveReport'sdeleteMessageis a hard delete and removes the stored file with it. Disable the account first, file the CyberTipline report, keep the report row and the file until the preservation period has run, and only then remove the content. - Nothing about this is discretionary and nothing about it waits for a queue.
Blip also does not run any automated CSAM detection -- no hash matching, no PhotoDNA, no scanning of uploads. Detection depends on a person reporting it. That is a real limit and it is stated rather than implied.
The honest limits
- One operator. Andrew Bui reviews reports. There is no team, no shift rota, and no escalation path beyond him.
- No 24/7 coverage, and no promised response time. He has a day job. A report filed at 2am is read when he next works the queue. Nowhere in the app, the Terms, the Privacy Policy or this page is a response time promised, because there is no process that could honour one.
- No automated content scanning and no proactive detection. Nothing looks at messages except a person, after a report. There is no filter on text, no classifier on images, no rate-based abuse detection.
- No filtering of message content. Nothing checks a message, a photo or a video against a word list or a classifier before it is shown. Blip is private messaging: there is no feed, no discovery surface and no public profile, so there is nothing to filter into. Protection is block, report, group removal by the admin, and deleting your account.
- Human review of everything is not offered. Reported content is reviewed. Unreported content is not seen.
- No formal appeals process. If an account is disabled and the person thinks that is wrong, the route is email to
safety@tryblip.app, and it reaches the same one person who made the decision. - Disabling an account does not erase it, and a device holding a valid token can act for up to an hour afterwards.
- Resolved reports are not deleted automatically. The retention policy is 12 months from resolution and there is no scheduled job enforcing it -- it is a thing the operator does by hand.
- Deleting your Blip account does not delete reports, in either direction. A report about you is the record of why something was removed, and letting it be erased by deleting an account and signing up again would defeat the point. A report you filed is evidence about somebody else, and is not yours to withdraw. The rest of your data goes; this is the exception, and it is a decision rather than an oversight.
Where this is verified in the code
| Claim | Source |
|---|---|
| Reporting, capture, rate limit, dedupe, report-implies-block | firebase/functions/src/reports.ts (submitReport) |
Operator actions, the moderator claim | firebase/functions/src/reports.ts (resolveReport) |
reports closed to every client | firebase/firestore.rules, firebase/tests/firestore.test.js |
| Captured text and reason never indexed | firebase/firestore.indexes.json |
| Blocking enforced server-side; the group exemption | firebase/firestore.rules (blockedByCounterparty) |
| Live typing torn down on a block | firebase/functions/src/blocking.ts (syncBlockedLiveTyping) |
| Live drafts removed on disconnect, never in Firestore | firebase/database.rules.json, firebase/tests/database.test.js |
| Reports survive account deletion, by decision | firebase/functions/src/account.ts |
| The report and block affordances, and their copy | Blip/Services/ReportService.swift, Blip/Features/Chat/ChatMessageActions.swift, Blip/Features/Chat/ConversationSettingsSheet.swift, Blip/Features/Messages/MessagesHoldOverlay.swift, Blip/Features/Messages/MessagesListRow.swift |
| Unblocking | Blip/Features/Profile/BlockedContactsView.swift |
| The published safety and support addresses | Blip/Features/Profile/AboutSheet.swift, Blip/Util/BlipLinks.swift |
| Behaviour under test | firebase/tests/reports.callable.test.js |
Last reviewed 17 September 2026, against the code in this repository. That pass re-checked every mechanism named above: the capture limits and flags, the ten-per-hour budget, the one-open-report rule, the rules that close reports to every client, the index exemptions, the moderator gate and what disabling an account does and does not do -- plus the blocking section, which was corrected because a block turns out to be symmetric for live typing, presence and reaction previews and this page had said it worked one way. Not re-checked by that pass, because it needs the app running rather than read: the navigation in "What you can do, and where it is".