-
Notifications
You must be signed in to change notification settings - Fork 0
Features
This page documents not just what was built, but why each feature exists β the motivation, the problem it solves, and the reasoning behind each decision.
| Route | Description | Auth |
|---|---|---|
/ |
Educational home | No |
/map |
Interactive map | No (limited) |
/colony/[id] |
Colony detail | No (limited) |
/colony/new |
Register a colony | Yes |
/learn |
Educational guide | No |
/learn/[slug] |
Educational article | No |
/reports |
Reports / resources / contacts / stories hub | Partial |
/impact |
Public impact | No |
/stories |
Stories wall (deep link into the hub) | Yes |
/resources |
Resource exchange (deep link into the hub) | Partial |
/contacts |
Contact directory (deep link into the hub) | No |
/glossary |
Glossary (36 terms) | No |
/plants |
Toxic plants | No |
/curso |
Caretaker course | No |
/profile |
User profile | Yes |
/u/[id] |
Public caretaker profile | No |
/notifications |
Notification inbox | Yes |
/login, /signup
|
Auth | No |
/forgot-password, /reset-password
|
Password recovery | No |
/cat/[id] |
Individual cat page | No |
/api/reports |
Report submission API route | Partial |
The decision of what requires login was deliberate: urgency outranks identification.
Someone who found a poisoned cat can't waste time creating an account. Someone who wants to understand what's happening on their street shouldn't have to sign up to read an article. The product earns trust before asking for commitment.
| Action | Auth | Why |
|---|---|---|
| View the map (approximate area) | β | Geographic context is the entry point |
| View a colony's narrative | β | Reading is the first step of conversion |
| Read the educational guide | β | Education can't have a barrier |
| Report a sighting / lack of food or water | β | Urgency > identification |
| Report an injured cat, poisoning, abuse | β | Emergencies don't wait on signup |
Use the help flow (/help) |
β | Same as above |
| See a colony's exact location | β (caretaker only) | Physically protects the animals |
| Confirm/resolve reports | β | Responsibility requires identity |
| Register a colony | β | Long-term commitment |
| Link as a caretaker | β | Same |
| Log feeding or water | β | A care action requires a link |
| Stories | β | Community content requires identity |
Why it exists: this is the point of the product. Converting a non-cat-person doesn't happen by decree. It happens when the person understands, on their own time, without pressure. The guide is the product's educational backbone β it's where Felines proves it isn't just another cat-person app, and it's the foundation everything else (the map included) builds on: the platform's job is to turn understanding into action, and mapping is one of the actions that understanding leads to, not a separate goal competing with it.
What was thought through:
- 19 articles in 5 progressive levels β the level structure isn't hierarchical, it's narrative. The reader starts by understanding the world, moves to the problem, reaches the impact, and only then finds what they can do. This sequence was designed so no comprehension step gets skipped.
- Titles written for search β "How to get rid of stray cats" converts better than "Why removing cats doesn't work." The title speaks to whoever has the problem, not to whoever already agrees with the solution.
- Fact chips with sources β every cited fact has an origin. The product never asks the reader to trust it blindly β it presents the evidence.
- Personalization quiz after 3 articles β the quiz only appears after some real engagement, so classification (Observer, Backup, Guardian) is based on something genuine. No wrong answer: the product doesn't judge anyone.
- Caretaker course with a one-time certification β the "Cuidador Preparado" certification can't be re-earned. That's not a limitation β it's the value. A badge anyone can get as many times as they want means nothing.
- Glossary linked from the profile β accumulated knowledge is part of the user's identity on the product. Seeing glossary progress inside the profile reinforces that learning is a form of contribution.
- A separate neighborhood-diagnosis quiz β distinct from the 3-article personalization quiz, this one asks what the reader actually sees on their own street (4 questions) rather than how they engaged with the guide, giving a second, situational entry point into the same classification system.
One of the actions the educational guide leads to β not a separate goal. Once someone understands why colonies exist and why they matter, the map is where that understanding becomes something concrete and local.
Why it exists: the map is the product's heart β it's where the abstract world of stray cats becomes concrete and local. Without it, Felines would be another information website. With it, a citizen sees that there's a colony on their own street, with a name, a story, and real caretakers.
What was thought through for each element:
- 3 pin types β the visual distinction between colony (permanent, has caretakers), sighting (temporary, no caretakers), and emergency (immediate urgency) guides action: each color has a different meaning and a different next step.
- Automatic clustering β without it, a city with many colonies becomes an illegible map. Clustering preserves clarity without hiding data.
- Side activity panel β caretakers need to see what's happening in the area they're looking at, not the whole world. The panel updates as the map moves, keeping local context.
- Real weather banner β extreme weather directly affects animal welfare. A 38Β°C heat alert or a heavy-rain alert has motivated caretakers to make extra check-ins. Weather is care information, not decoration. The card shows the real city name, temperature, and conditions for the map's current center, and updates as the map is dragged to a new location β it's not fixed to one city. Dragging from Natal to Fortaleza should show Fortaleza's weather, not Natal's; a static banner would silently lie the moment someone pans away from the default view. The underlying OpenWeatherMap request is also made in the visitor's current site language, so the condition text (e.g. "scattered clouds" vs. "nuvens dispersas") matches PT/EN, not just the UI around it.
- A lock icon appears on pin hover for any colony whose location the current visitor can't see exactly β a small, constant reminder that what's shown is an approximation, not just a one-time disclosure buried in a popup.
- Need heatmap β lets protectors and NGOs identify where the situation is most critical before opening each colony individually.
- "Not sure what you're seeing?" badge β Felines' primary user isn't a cat person. They may have never seen a colony map before. This badge removes the entry barrier without dumbing down the product.
- Redesigned sighting popup β icon header, approximate-location badge, clear CTA β matching the visual hierarchy already used for colony popups.
- 3-flag system β removal + ban β without a moderation mechanism, anyone could register fake pins to confuse or harm. Three flags distribute responsibility and prevent a single bad-faith user from abusing the system. See Security for the full mechanism.
Why it exists: real cases of cats poisoned after their location was shared online informed this decision. The blur isn't a data-privacy feature β it's a physical animal protection feature.
| Level | User | Precision | Offset |
|---|---|---|---|
| 1 | Anonymous | Neighborhood | ~500m + visible uncertainty circle |
| 2 | Authenticated, not caretaker | Street | ~100m |
| 3 | Linked caretaker or creator | Exact | Via a server-side function that reconfirms the link on every call |
The visible uncertainty circle at level 1 was a deliberate honesty decision: the user knows they're seeing an approximate location, and knows why.
Why it exists: the colony page is where information becomes relationship. A pin on a map is abstract. A colony with a name, photo, story, named cats, and real caretakers creates a connection. A visitor who wasn't a cat person can read about a specific orange cat that hasn't been seen in 5 days, and feel that it matters.
What was thought through:
- Cover photo with timeline history β overwriting the photo would erase the colony's visual memory. Every new cover-photo upload creates its own timeline event rather than silently replacing the old image in place, so the colony's visual history stays intact and browsable in the Timeline tab.
- Auto-calculated castration badge β a manually set status would drift out of date easily. Calculating it from registered cats guarantees the badge always reflects what's actually recorded.
- Photo upload not required β mandatory upload was blocking colony registration in urgent situations. The photo matters, but can't matter more than the registration itself.
- Contextual buttons (caretaker vs. visitor) β a visitor seeing "Stop caring for this colony" gets confused. A caretaker not seeing care actions gets frustrated. Contextual separation cleans up the interface without hiding functionality.
- "Stop being a caretaker" β caretakers move cities, change life circumstances, lose capacity. Trapping someone into a colony with no way out would be irresponsible. A dignified exit is part of designing a healthy community.
- Community verification (3 confirmations) β distributes the responsibility of validating that a colony genuinely exists. You cannot verify your own colony β it requires outside perspectives.
-
Colony health index (0β100, auto-calculated) β a single
derived score (
recalculate_colony_health()), never set manually, that factors in things like castration progress and how recently the colony's been checked in on. It exists so a caretaker (or an NGO scanning the map's heatmap) can tell "this colony is doing fine" from "this one needs attention" at a glance, without reading the whole timeline first. - Rotating contextual facts β the colony page surfaces a changing cat fact/tip alongside the content, the same "moment of lightness" reasoning as the cat assistant, but ambient rather than triggered.
Cats β the cats are the reason for everything. Naming each
one creates identity and responsibility. The "unseen for 7+
days" radar exists because a cat that disappears might be sick
or hurt β and the caretaker needs to know. Cat notes
(cat_notes) are intentionally immutable β any logged-in
user can add one, but it can't be edited afterward, since an
observation note is evidence, not an editable opinion.
Timeline β the chronological log turns the colony into a narrative. Anyone visiting months later can understand what happened, who cared for it, when, and how. Shared colony stories also surface here.
Needs β three deliberately separate systems: general help requests (food, foster homes, transport, medication β anything ad hoc), neutering requests (castration is the platform's core intervention and deserves its own trackable status pipeline: open β in progress β completed), and the resource-exchange board. Recurring care reminders (feeding/water/health-check/shelter-check) also live here, with overdue/due-today badges computed on read.
Reports β every report filed against this colony, in one place.
Caretaker Letter β caretakers disappear without notice. The letter is a way to pass the baton with dignity β telling the story, the cats' habits, what works. Version history exists because the letter evolves over time.
(Edit history is tracked but surfaced inline rather than as its own separate tab in the current UI.)
Why it exists: when someone finds a poisoned cat or witnesses abuse, they don't want to read an article. They need to know what to do right now. The emergency flow exists for that moment β no signup, no friction, with guidance specific to each situation.
What was thought through:
- Global modal from any page β an emergency has no URL. The user could be on any page when they encounter the problem. The modal guarantees access is always one click away.
- 9 situation buttons β sighting, conflict with the cats, injured/sick cat, suspected poisoning or abuse (a single combined option β both call for the same first steps: document evidence, don't confront anyone, contact the same authorities), lone kitten, construction/eviction threat to a colony, missing cat, disease outbreak, and other. A lone kitten has its own protocol (wait and observe before acting); abuse/poisoning requires evidence before acting; generalizing across all of these would be dangerous.
- Real local resources, in both languages β the product never just says "seek help." The abuse/poisoning alert names Disque DenΓΊncia (181, anonymous) and Emergency Services (190), and cites both the base animal-cruelty law (Lei 9.605/98) and the SansΓ£o Law (Lei 14.064/2020, aggravated penalties specifically for dogs and cats). The disease-outbreak alert names the CCZ (zoonosis control center) directly. This was found to be English-only-incomplete during a final content audit β the Portuguese text had the real phone numbers, the English text didn't β and was fixed so both languages cite the same specific resources, not just one of them.
Why it exists: protecting stray cats depends on information circulating β who saw what, where, when. Reports are the collective vigilance mechanism, without vigilantism: they report situations, never people.
What was thought through:
- Auto-resolve at 3 confirmations β a single person doesn't have authority over reality. Three independent confirmations distribute responsibility and increase the system's trustworthiness. The function is atomic in the database to prevent race conditions.
- Sensitive reports with a permanent record β poisoning, abuse, and disease outbreaks can never simply be "resolved and forgotten." The permanent timeline record serves as historical evidence and a warning for whoever comes next.
- Rate limiting in 2 layers β the 10-reports/hour anonymous and 30/hour authenticated limits protect against spam and fake coordinates. The second layer (database circuit breaker) guarantees protection even if the first is bypassed.
- Reports without login for urgent situations β allowing anonymous reports was an ethical decision: requiring signup to report an injured cat would place bureaucracy above the animal.
Why it exists: caring for colonies is expensive. Food, vet transport, medication, time β informal protectors often operate alone with their own resources. The resource exchange connects who has with who needs, within the product's own ecosystem.
What was thought through:
-
Resource exchange expiring in 30 days β resources that stay
active indefinitely lose credibility. Expiry forces updates and
keeps the board relevant. Posts split into Available and
Wanted; marking interest in someone else's post notifies its
author (
resource_interest), and a poster can never express interest in their own post β self-interest is blocked the same way self-thanking and self-confirmation are. "Help received" lets the author close the loop once the exchange actually happened. - Stories visible only to logged-in users β restricting stories was about community quality. Stories are emotional, personal content β requiring identity creates accountability for what gets published.
- 14 real Natal/RN contacts β no contact was generated or estimated. Each was researched and manually verified. The directory is human curation, not an automated aggregator.
Why it exists: caretakers need to know what's happening without opening the app every hour. Notifications exist to bring relevant information at the right moment β not as forced engagement, but as real utility.
| Type | What triggers it | Who receives it |
|---|---|---|
action_thanks |
Someone thanks a specific timeline action | Whoever performed the action |
help_request_response |
Someone responds to a help request | All colony caretakers |
sighting_cluster |
3+ sightings within ~200m become a suggested colony | Caretakers within a 500m radius |
resource_interest |
Someone marks interest in a resource post | Whoever posted the resource |
story_reaction |
Someone reacts to a story | The story's author |
extreme_weather |
Extreme weather in the region | Caretakers of each colony (1/day/colony) |
cat_unseen |
A cat's "last seen" hasn't updated in 7+ days | Colony caretakers (1/cat/day) |
area_alert |
A serious report within a 5km radius | Nearby caretakers (fixed template) |
report_submitted |
A new report on a colony | Colony caretakers |
colony_removed_ban |
A colony was removed by 3 false-pin flags | The colony's creator |
What was thought through for each type:
-
extreme_weatherβ heat above 32Β°C or heavy rain directly affect cat welfare. The notification is contextual care information, not a generic alert. -
cat_unseenβ 7 days without a "last seen" update is enough time to worry. The alert fires per cat, not per colony, so the caretaker knows exactly which animal needs attention. -
area_alertβ serious reports within 5km affect any nearby colony. The message uses a fixed template (never the reporting user's free text) to prevent malicious or alarmist content injection. -
sighting_clusterβ repeated sightings at the same spot suggest an unmapped colony. The notification invites nearby caretakers to investigate before a formal colony is created. -
colony_removed_banβ transparency about removal. The creator knows the colony was removed, why, and what their ban status is. Without a notification, silent removal would be unfair.
Notifications can be marked read individually, not just all at once, and the navbar shows a live unread-count badge on the user's avatar so there's an at-a-glance signal without opening the inbox.
Nothing here runs on a cron job β everything is checked when the
app loads. This isn't limited to notifications: help-request
expiry (7 days), resource-post expiry (30 days), care-reminder
overdue/due-today badges, and the cat-unseen radar are all
computed at read time from a stored timestamp, not by any
background worker. There is no scheduled job runner anywhere in
this stack β see Behind the Scenes and
lib/data/notifications.ts's own header comment for why.
Why they exist: badges recognize contributions without creating competition. Each badge marks a real action β not an accumulated point count. The goal is to say "you did this," not "you did more than that person."
| Badge | Condition | Why it exists |
|---|---|---|
| π€ Caretaker | Linked to β₯1 colony | Marks the formal bond with a colony |
| πΎ Registered Colony | Created β₯1 colony | Recognizes whoever mapped it |
| π½οΈ Fed | Made β₯1 feeding record | Recognizes the most basic, most frequent action |
| π₯« Donated | Posted β₯1 offer in the resource exchange | Recognizes resource contributors |
| π¨ Reported | Submitted β₯1 report | Recognizes community vigilance |
| π Thanked | Received β₯1 caretaker-level thank-you | Recognizes who the community values |
| π Certified | Completed the course + certification | Recognizes formal training β a unique, non-repeatable badge |
Why it exists: caring for stray cats is a habit, not a one-time event. The streak exists to recognize consistency, not volume. A caretaker who visits their colony every day deserves that recognition, even if each visit is simple.
What was thought through:
- Resets to 1, not 0 β fully zeroing out after one missed day would be punitive and demotivating. Resetting to 1 acknowledges that care continues, even if the run broke.
- Always private β a public streak would create competition. A caretaker with a 200-day streak isn't better than one with a 3-day streak who just started. The product doesn't compare people.
-
Two distinct streak types β
record_daily_visit(visiting the profile) andrecord_care_streak(acting on a colony you care for) are intentionally separate because they represent different kinds of engagement: one is presence, the other is action.
Why there are two: thanking a specific timeline action
(action_thanks) and a generic thank-you to a caretaker
(thanks) serve different purposes. Mixing them would create
ambiguity about what's actually being recognized.
action_thanks β "thank you for yesterday's feeding":
- Notifies whoever performed the action β direct, specific recognition
- 1 per person per event β prevents thank-you spam
- No self-thanking β blocked in the database, not just the frontend
thanks β "thank you for caring for this colony":
- No notification β a quiet gesture, not a demand for attention
- Feeds the π badge β accumulated recognition
- 1 per (colony, caretaker, sender) β each relationship gets its own thank-you
Why it exists: colonies emerge organically β nobody creates them, they exist. When multiple sightings cluster in one area, there's probably an unmapped colony. Automatic clustering detects this pattern before it needs manual intervention.
What was thought through: a suggestion never becomes a colony on its own. Automation detects, but a human decides. This prevents false positives and keeps map quality high.
Why it exists: inspired by Microsoft Office's Clippy β the cultural memory of an animated character that appears contextually with a relevant message. The difference is that Felines' cat appears at carefully chosen moments, not randomly, and leaves on its own.
What was thought through for each trigger:
- First visit β the welcome moment. A cat curiosity humanizes the product before any CTA.
- Idle on home / reports / colony / profile β the user is present but without direction. The cat offers non-intrusive guidance.
- End of article β recognizes that the user invested time. A fun curiosity as a reward.
- First colony registered β the product's biggest conversion milestone deserves a special moment.
- Empty map area β an invitation to be the first to map, without pressure.
- Quiz completed β celebrates educational engagement.
Why curiosities, not data: serious data already exists in the
fact chips and on /impact. The assistant is the moment of
lightness and delight β mixing the two would dilute both.
| Trigger | Where | Condition |
|---|---|---|
| 1 | Home | First-ever visit |
| 2 | Home | 45s idle |
| 2B | /reports |
45s idle |
| 2C | /colony/[id] |
45s idle |
| 2D | /profile |
45s idle |
| 3 | /learn/[slug] |
95% scroll (end of article) |
| 4 | After /colony/new
|
First colony registered |
| 5 | /map |
Empty area on the map |
| 6 | /learn |
Quiz completed |
- 15-minute cooldown between appearances (checked via
sessionStorage/localStorage, not a server-side timer) - Never interrupts an open modal or active typing
- Respects
prefers-reduced-motion: shows a static speech bubble with no video at all, rather than trying to show a paused or degraded animation - The video plays once and the component unmounts naturally on end β there's no loop, and no lingering floating element after the moment has passed
- The bubble can also be dismissed early with its own close button
- Curiosity text matches the visitor's current site language (PT/EN)
- Draws from a set of real cat curiosities, never platform statistics
Cmd/Ctrl+K modal β searches colonies, articles, and the glossary. A product with 19 articles, 36 glossary terms, and many colonies needs a search shortcut so users don't need to know which page holds which information.
Why it exists: Felines needs to prove it works. The impact page is the public answer to "does this make a difference?" Real-time statistics and a recent-activity feed show the product is alive and that people's actions have measurable effect.
TNR projection calculator β an interactive tool where a visitor adjusts a colony's size and a monthly neutering rate and watches a population projection update in real time, making the vacuum effect (see Research and Benchmark) visible as a curve instead of just a sentence in an article. The methodology and assumptions behind the projection are documented alongside it, since a number with no visible reasoning behind it isn't more convincing than the claim it's illustrating β it's the same "show the evidence, don't just assert it" principle that shapes the fact chips throughout the guide.
Why it exists: a caretaker's public profile is the credibility layer behind a colony β when a colony page shows "cared for by Maria," this is where a visitor (or a future caretaker deciding whether to link themselves to the same colony) can see that Maria is a real, active person, not just a name.
What's shown: avatar, display name, public contact if the caretaker chose to set one, earned badges, colonies currently cared for, reports submitted, and the "Cuidador Preparado" certification if earned. A flag button lets any visitor report the profile itself, using the same moderation path as flagging a colony or a report β profiles aren't exempt from the same accountability the rest of the product has.
What's deliberately not shown: streaks (always private, see Care Streak) and anything from the account's private profile fields β this page only ever renders the same public-column grant anonymous visitors already have access to, never a privileged view.
A signed-in user can follow a colony without becoming a caretaker
β a lighter-weight relationship for someone who wants to keep an
eye on a specific place without taking on care responsibility.
The notify_followers() function and the underlying
colony_followers table are fully built and ready to use, but no
event in the current product actually calls it automatically yet
β following exists as real, working infrastructure for a feature
that hasn't been wired up to a trigger, rather than a stub. See
Database for the table detail.
Felines' security goes beyond data protection. It's physical protection of the animals. Full detail in Security.
- RLS on ~26 tables β no table is left unprotected. Every access is validated against the user's identity and role.
- Progressive blur with column-level grants β the split between exact and blurred columns happens in the database, not the frontend. It can't be bypassed client-side.
- Hardened RPCs β notifications use fixed server-side templates. No free user text reaches notification content β eliminating a spam/phishing vector.
- Rate limiting in 2 layers β the first layer (API) is convenient. The second (database circuit breaker) is the one that actually matters β it works even if the first is bypassed.
-
Geographic range
CHECKconstraints β coordinates outside the valid geographic range are rejected in the database. They never even reach processing. - MIME/extension allowlist β blocks malicious SVGs and other types that could be used for XSS via image upload.
-
Open-redirect protection β the
returnToparameter only accepts internal paths. It can never redirect to an external URL. - Path traversal blocked β fixed after Aikido Security (HIGH). The upload path prefix is sanitized before reaching storage.
- SSRF fixed in geocoding β fixed after Aikido Security (LOW). Coordinates are validated before any external API call.
- 3-false-pin-flag ban system β distributed moderation that removes a repeatedly-flagged colony and bans its creator, escalating to a permanent ban on the 3rd offense.
Full audit documented in docs/AUDIT_REPORT.md.