Blip
Back to tryblip.app

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

What you can do, and where it is

You want toWhere it is
Report a messagePress and hold the message, then Report message
Report a personOpen the conversation, tap the menu in the header, then Report user
Report a person, from the inboxSwipe a one-to-one row and tap the flag, or press and hold the row and choose Report
Block someoneThe conversation's header menu, then Block user -- or press and hold their row in the inbox, then Block
Unblock someoneProfile then Blocked contacts, then the icon on their row
Email a person about itProfile 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:

Three things blocking does not do, stated because you should not have to guess:

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.

CapturedDetail
The message textcaptureText 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
MediaThe storage path of an attached photo or video, not a second copy of the file
Whether it was already retractedcapturedWasRetracted. 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 itThe 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 itThe reporter's account id, taken from their authenticated session
WhereThe conversation id, and whether it is a one-to-one or a group
Who they were at the timeThe reported account's display name and username when the report was filed, since anyone can rename before it is read
WhenThe time the message was sent, and the time the report was filed
A reasonAn 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:

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:

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:

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:

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:

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

Where this is verified in the code

ClaimSource
Reporting, capture, rate limit, dedupe, report-implies-blockfirebase/functions/src/reports.ts (submitReport)
Operator actions, the moderator claimfirebase/functions/src/reports.ts (resolveReport)
reports closed to every clientfirebase/firestore.rules, firebase/tests/firestore.test.js
Captured text and reason never indexedfirebase/firestore.indexes.json
Blocking enforced server-side; the group exemptionfirebase/firestore.rules (blockedByCounterparty)
Live typing torn down on a blockfirebase/functions/src/blocking.ts (syncBlockedLiveTyping)
Live drafts removed on disconnect, never in Firestorefirebase/database.rules.json, firebase/tests/database.test.js
Reports survive account deletion, by decisionfirebase/functions/src/account.ts
The report and block affordances, and their copyBlip/Services/ReportService.swift, Blip/Features/Chat/ChatMessageActions.swift, Blip/Features/Chat/ConversationSettingsSheet.swift, Blip/Features/Messages/MessagesHoldOverlay.swift, Blip/Features/Messages/MessagesListRow.swift
UnblockingBlip/Features/Profile/BlockedContactsView.swift
The published safety and support addressesBlip/Features/Profile/AboutSheet.swift, Blip/Util/BlipLinks.swift
Behaviour under testfirebase/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".