PetitionMeUpset (HumanAngry NeedOtherHumanToComplain?)

PetitionMeUpset: A Cross-Platform Petition Platform Built for Replit Power Users

Hey everyone,

I wanted to share a project I’ve been working on: PetitionMeUpset, a mobile and web app that lets Replit power users create, sign, and upvote petitions demanding Replit improvements. Whether it’s a long-overdue editor feature, a pricing change, or a deployment quality-of-life fix, this is the place to make your voice heard, with your identity fully protected if you want it to be.

https://petitionmeupset.replit.app/mobile/


What Is It?

PetitionMeUpset is a cross-platform petition platform built specifically for the Replit community. The core idea is simple: if enough people care about something, a petition with real signatures is more compelling than a forum thread. You can browse petitions by category, sign publicly with your name attached, or sign completely anonymously, with strong privacy guarantees on the anonymous path (more on that below).


Features

  • Petition feed: browse all petitions sorted by most recent, most signed, or trending (signatures in the last 48 hours). Filter by category or search by keyword.
  • Categories: petitions are tagged as one of: editor, deployments, pricing, ai, mobile, web, or other.
  • Create petitions: write a title and body, pick a category, set an optional signature goal, and choose whether to post under your name or anonymously.
  • Sign publicly or anonymously: every petition has a sign sheet where you choose your signing mode before submitting. Public signatures show your display name; anonymous signatures show nothing, not even that you exist in the database.
  • Upvoting: separately from signing, you can upvote petitions to signal interest. Upvotes toggle on/off and are always linked to your account (no anonymous upvotes).
  • Petition management: owners can edit, close, or delete their own petitions.
  • Comments: threaded comments per petition for discussion.
  • Cross-platform: a React web app and an Expo React Native mobile app, both connected to the same backend.

How to Use It

1. Sign in

Both the web app and the mobile app use Replit Auth (OIDC/PKCE). On web, you’re redirected through the Replit OIDC provider. On mobile, the PKCE flow runs through expo-auth-session, exchanges the authorization code for a server-side session token, and stores it securely in expo-secure-store.

2. Browse petitions

The home feed lists petitions sorted by most recent by default. Use the category filter chips to narrow to a specific area (e.g., pricing or ai). Use the search bar to find petitions by title or body text. You can also switch the sort to Most Signed or Trending.

3. Create a petition

Tap the Create tab or button. Fill in a title, write the petition body, pick a category, and optionally set a signature goal. You can also choose to post the petition anonymously. Your Replit user ID will still be associated server-side so you can edit, close, or delete your own petitions, but your name will not appear publicly.

4. Sign a petition

On a petition’s detail page, tap Sign. A bottom sheet appears with two options:

  • Sign publicly: your display name is stored alongside the petition. Other users can see your name in the signatories list.
  • Sign anonymously: see the privacy section below.

5. Upvote

On any petition card or detail page, you can upvote to show support without signing. Upvotes toggle: tap again to remove yours.


Privacy: Anonymous Signing

This is the part I’m most proud of getting right.

When you sign anonymously, the server inserts a signature row with:

petitionId: <id of the petition>
signedAt:   <current timestamp>
userId:     NULL
displayName: NULL

That’s it. No user ID. No hash. No fingerprint. No correlation whatsoever between your session and that row. Even if you gave a database administrator full read access to the signatures table, they could not determine who signed anonymously. The only information that exists is that someone signed that petition at that time.

This is enforced at the application layer and is also reflected at the database constraint level: the unique index that prevents duplicate public signatures explicitly excludes rows where userId IS NULL, so anonymous signatures are unconstrained. You can sign anonymously even if you previously signed publicly.

The mobile app’s settings screen and the web app’s privacy page both explain this model to users.


Tech Stack

For anyone curious about the architecture:

Backend

  • Express 5 on Node.js 24
  • PostgreSQL database with Drizzle ORM for schema definition and type-safe queries
  • Zod (zod/v4) for runtime validation of all request bodies and query params
  • Replit OIDC/PKCE auth via openid-client v6 (functional API, no classes)
  • Server-side session store in PostgreSQL (TTL-enforced sessions table)
  • Mobile auth via a /api/mobile-auth/token-exchange endpoint that exchanges PKCE authorization codes for session tokens

Web Frontend

  • React + Vite
  • TanStack Query for data fetching and cache management
  • Shadcn/Radix UI component library
  • Wouter for client-side routing
  • TypeScript throughout

Mobile App

  • Expo 54 with Expo Router 6 (file-based routing)
  • React Native + TanStack Query
  • expo-auth-session for PKCE login flow
  • expo-secure-store for secure token storage
  • Dark theme: near-black background (#0D0D0F), red/orange primary accent (#FF3B30), Inter font family

Shared Infrastructure

  • pnpm monorepo with workspace packages
  • OpenAPI 3.1 spec hand-authored in lib/api-spec
  • Orval codegen generates typed React Query hooks (lib/api-client-react) and Zod schemas (lib/api-zod) from the OpenAPI spec. Both the web and mobile apps consume these generated clients, so the API contract is always in sync.
  • TypeScript 5.9 with composite project references across all packages

Database Schema (key tables)

Table Purpose
users Replit OIDC user records (id, email, firstName, lastName, profileImageUrl)
sessions Server-side session store for mobile auth tokens (TTL enforced)
petitions id, title, body, category, createdByUserId, isAnonymous, signatureGoal, closedAt, createdAt
signatures id, petitionId, userId (NULL for anonymous), displayName (NULL for anonymous), signedAt
upvotes id, petitionId, userId (NOT NULL), createdAt, unique on (petitionId, userId)
comments id, petitionId, userId, body (max 500 chars), createdAt

API Endpoints

GET  /api/auth/user                        current authenticated user
POST /api/mobile-auth/token-exchange       exchange PKCE code for session token
POST /api/mobile-auth/logout               invalidate session token
GET  /api/petitions?category=&search=&sort= list petitions
POST /api/petitions                        create petition (auth required)
GET  /api/petitions/:id                    petition detail + signatories
PATCH /api/petitions/:id                   edit petition (owner only)
DELETE /api/petitions/:id                  delete petition (owner only)
PATCH /api/petitions/:id/close             toggle petition open/closed (owner only)
POST /api/petitions/:id/sign               sign petition (auth required)
POST /api/petitions/:id/upvote             toggle upvote (auth required)
POST /api/petitions/:id/comments           add comment (auth required)
GET  /api/petitions/:id/comments           list comments

What’s Next

A few things on the roadmap:

  • Notifications when a petition you signed reaches its goal
  • Better discoverability (trending section, featured petitions)
  • Richer petition analytics for petition owners
  • Native share links

Try It Out

Both apps are live and connected to the same backend. Sign in with your Replit account, browse the petitions that are already there, and add your voice, publicly or anonymously.

If there’s a Replit feature or fix you’ve been wanting forever, create a petition. A well-written petition with a real signature count is harder to ignore than a forum comment.

If you’re a developer and want to explore the codebase or contribute, everything is in a pnpm monorepo with TypeScript throughout. The OpenAPI spec in lib/api-spec is the single source of truth for the API contract.

Looking forward to seeing what the community has to say, and what you want Replit to build next.


Built on Replit, for Replit.

Drop Down For Prompt

PetitionMeUpset Mobile App

What & Why

Build a mobile app (Expo) for Replit power users to create and sign petitions demanding Replit implement features or fix problems. It is a platform for expressing dissatisfaction and organizing community pressure — think angry but constructive. Users authenticate with Replit Auth so they have a real identity on the platform, but can choose to sign any petition fully anonymously with strong privacy guarantees.

Done looks like

  • Users log in with Replit Auth before accessing the app
  • A petition feed screen shows all active petitions with title, description excerpt, signature count, category tag, and an angry/boo reaction icon
  • Users can tap a petition to view the full detail: body text, total signatures, and a list of public signatories (anonymous entries show only “Anonymous”)
  • A “Create Petition” flow lets users enter a title, body, and category, then submit it
  • Signing a petition lets the user choose “Sign Publicly” (Replit username shown) or “Sign Anonymously” (no identifying info stored or derivable — even admins and Replit staff cannot link the signature back to a user)
  • A settings screen shows the logged-in user’s Replit identity and their default signing preference
  • All data persisted via the shared Express API server and the Replit PostgreSQL database
  • Sensitive config (session secrets, etc.) stored only in Replit Secrets — never in replit.md, source files, or committed config

Privacy Architecture

Anonymous signatures must be truly unlinkable. The server must:

  • NOT store the user ID alongside any anonymous signature row
  • NOT log the user’s identity during an anonymous signature request
  • NOT expose any correlation between a user session and their anonymous signatures, even in server logs or DB admin views
  • Use a server-side flag to strip identity before writing: if the user chooses anonymous, only a timestamp and the petition ID are stored — nothing else

Out of scope

  • Voting/downvoting or commenting on petitions
  • Email notifications or push alerts
  • Moderation tools

Tasks

  1. Replit Auth integration + OpenAPI spec + backend — Integrate Replit Auth (OpenID Connect) into the Express API server. Extend the OpenAPI spec with petition and signature endpoints (list petitions, create petition, sign petition with anonymous flag, list public signatories). Implement route handlers with auth middleware, Drizzle schema for petitions and signatures tables (anonymous rows store NO user identifier), and push the schema to the Replit database. Store any secrets (session secret, OAuth credentials) only in Replit Secrets.
  2. Scaffold Expo artifact + codegen + mobile frontend — Call createArtifact for the Expo app (slug: mobile, previewPath: /), run codegen to generate React Query hooks, then hand off to a design subagent to build the full mobile frontend. The app should implement Replit Auth login, petition feed, petition detail, create petition flow, sign petition sheet (public/anonymous choice), and settings screen. Design aesthetic: frustrated but polished community app — dark accents, bold typography, emoji reactions (:face_with_steam_from_nose::fire::skull::-1:).

Relevant files

  • lib/api-spec/openapi.yaml
  • lib/db/src/schema/index.ts
  • artifacts/api-server/src/routes/index.ts
  • artifacts/api-server/src/routes/health.ts
  • artifacts/api-server/src/app.ts
  • lib/api-spec/orval.config.ts
  • lib/api-client-react/src/index.ts
  • lib/api-client-react/src/custom-fetch.ts
  • .local/skills/replit-auth/SKILL.md