I'm In is a lightweight community activity app for organizing, sharing, discovering, and joining real-world activities.
It is built to work alongside the chat groups people already use. The app does not try to replace WhatsApp-style coordination; it adds a cleaner way to publish an activity, track who is coming, handle waitlists, share the right link, and give hosts a simple management view.
The software code is available under AGPL-3.0 (see LICENSE and LICENSING.md).
The "I'm In" name, logo, marks, and visual identity are not licensed under AGPL. If you fork or deploy this project, you must use your own name and branding and must not imply affiliation with the official I'm In or Lalo-operated services.
The app is an active early-stage product with substantial real functionality, but it is still evolving.
What is solid today:
- activity creation and editing
- public / semi-public / private visibility modes
- signed-in and guest RSVP flows
- waitlist behavior and cancellation promotion
- proxy RSVP / "add another person"
- guest session persistence and guest bookings
- host dashboard and co-host support
- WhatsApp verification/linking with persisted verified WhatsApp numbers when returned by Lalo
- in-app notifications, including host-sent activity messages and guest replies back to hosts
- public browse/search
- public browse/search with a subtle "other activities this week" count for hidden upcoming activity volume
- Google Calendar and
.icsexport from joined activities - "thinking about it" interest tracking
- lightweight platform moderation for public-facing activity content, with a hidden admin review queue and a public transparency log
- mobile modal behavior across the main user flows, including sticky headers, internal modal scrolling, background-page scroll locking, and no forced keyboard pop on open for the main form modals
What is still rough or partial:
- guest recovery is only partially realized as a product flow
- schema truth is split across multiple SQL files
- automated verification is still light and focused, not broad regression coverage
- some older docs/checklists are historical and not the full source of truth
im-in is responsible for:
- the activity/community product experience
im-inUI, product workflows, and Supabase-owned app logic- feedback intake and the app-side integration code that calls shared Lalo services
im-in is not responsible for:
- shared auth or WhatsApp verify ownership
- engineering control-plane ownership
- worker execution
Shared dependencies:
lalo-verifyfor auth, identity, and WhatsApp verify flowslalo-platformfor engineering work items and prompt-generation APIslalo-workersonly indirectly through shared service contracts
Although the codebase still uses many event names internally, the live product language is activity / activities.
Main product areas:
- a public home page for everyone, with browse, create, transparency, and community/about messaging
- public activity browsing through
/calendar - a signed-in dashboard through
/my-activities - activity detail pages with RSVP, proxy RSVP, cancellation, sharing, and host contact actions
- a create/edit flow with delayed authentication
- a 3-step create/edit flow with visibility first
- a host dashboard for attendee, host, and access-request management
- a guest bookings/recovery flow for users operating without a full signed-in account experience
- shared modal UX patterns that keep long sheets usable on mobile without scrolling the page behind them
public: publicly discoverable, full public-facing details shownsemi_public: appears in public browse with limited details; host shares the private access link manuallyprivate: unlisted / link-only- host names are always shown on public and semi-public activity surfaces
publicactivities can be gated by lightweight platform moderation and host trust before broader browse visibility is enabledsemi_publicpublic-preview content also goes through moderation and can appear in the public transparency log, while its private-link-only content stays outside platform moderation reviewprivateactivities stay outside platform moderation review and do not appear in the public moderation transparency log- the public browse page can also show a subtle count of upcoming activities that are happening this week but are not currently visible in public discovery, excluding items marked as spam
starts_atis stored in UTC- each activity also stores a
timezone - duration is stored as
duration_minutes - create/edit authoring uses 15-minute increments
- signed-in RSVP
- guest RSVP
- per-activity host setting for whether guest email is required to join
- waitlist placement when full
- optional host approval before membership is granted
- pending-approval attendees appear in
Goingwith clearPending host approvallabels - cancellation with promotion
- proxy RSVP / "add another person" (approval-aware when host approval is enabled)
- host-added attendees
- "thinking about it" interests
- create and edit activities
- duplicate activities
- manage attendees and waitlist
- approve/decline/request more info on semi-public access requests
- review and approve/reject join requests when host approval is enabled
- share public and private links
- add co-hosts
- see neutral discovery-status messaging when broader public visibility is limited
- allowlisted admins can use a hidden moderation page to review, archive, spam-mark, or manually override discovery decisions for public-facing activity listings
- browse public activities on
/calendar - open
/events/:slug - RSVP directly as a signed-in user or guest
- when guest email is optional for an activity, guests can join with just a name and add email later
- if host approval is enabled, submit a join request and appear in
GoingasPending host approvaluntil host review - if full, join the waitlist when allowed
- optionally add another person
- optionally mark "thinking about it"
- after joining, add to Google Calendar or download
.ics
- create via
/create-event - land on
/host/events/:idafter save - manage attendees, requests, hosts, sharing, and duplication
- edit through
/host/events/:id/edit - review pending join requests where pending attendees are already visible in
Going - hosts can always open Google Calendar and
.icsexport actions from the activity detail page
- Step 1: choose visibility
- Step 2: add activity details
- Step 3: set joining rules and host info
- field-level
Public/Privatebadges explain what is shown publicly and what stays behind the private link or after joining - in-flow back navigation keeps draft state while moving between steps
- signed-out users can fill the create form before authenticating
- draft state is stored locally
- on save, the app prompts for email and sends a magic link
- after sign-in, the user returns to Step 3 and only sees
One Last Stepname capture when profile details are still missing
- guest RSVP creates or reuses an
attendee_profilesrow plusattendee_sessionstoken - activities can allow guest join without email; those guests still get local guest-session continuity on the same device
- token is stored locally in the browser
/bookingsreads bookings using that guest session/recover?token=...can restore the guest session if a valid token exists
Important caveat:
- the "Find my bookings" screen in
/login?recovery=truecurrently sends a Supabase magic link, not a true guest-session recovery email guestService.sendRecoveryEmail()exists in code but is not wired into the UI and still contains TODO-level email delivery scaffolding
Current sign-in model:
- magic-link only
- Google sign-in is not part of the current app
Current identity model in practice:
- signed-in users use Supabase Auth
- guests use
attendee_profiles+attendee_sessions - signed-in users are also synchronized into the profile/session-backed attendee model
- WhatsApp verification/linking can enrich the canonical attendee profile with both
lalo_user_idand a verifiedwhatsapp_number - no-email guests can add email later to unlock account recovery and cross-activity identity continuity
- guest-email upgrades now merge identities through a dedicated SQL RPC so attendee ownership, inviter attribution, sessions, interests, and join requests move together
- when a user becomes authenticated on a device with a valid stored guest session, the app now auto-claims that guest identity only when the target signed-in profile is still shell-like and there is no overlapping attendee history
- when those safety checks fail, the app leaves guest and signed-in identities separate for now instead of attempting a risky silent merge
- when auto-claim declines for a promptable conflict, the app now shows a lightweight merge-or-keep-separate prompt instead of silently no-oping
- remembered guests now see a clearly labeled local guest-account state on Home, plus upcoming activities saved on that device and a sign-in CTA for cross-device continuity
Current hardening notes:
- signed-in profile hydration prefers the canonical
attendee_profilesrow over stale auth-email display during pending email-change confirmation - signed-in RSVP now ensures a profile exists before submit, matching the safer signed-in
thinking about itflow /loginnow redirects authenticated users to/my-activities
This means the app currently has a dual identity model, not a fully unified one.
For the longer-term migration direction, see AUTH_UNIFICATION_PLAN.md.
The app includes a global, lightweight prompt system for users who open links inside in-app browsers/webviews.
- Runtime detection lives in
src/utils/runtimeEnvironment.ts. - Prompt eligibility logic lives in
src/utils/installPromptEligibility.ts. - Local prompt persistence (dismiss timers + one-time post-verify follow-up) lives in
src/utils/inAppBrowserPromptState.ts. - Shared profile input for prompt eligibility lives in
src/hooks/useAttendeeProfile.ts. - Shared post-verify install-prompt request helper lives in
src/utils/postVerifyInstallPrompt.ts. - Global UI/controller lives in
src/components/system/InAppBrowserPrompt.tsx.
Current behavior:
- Prompts are considered in browser sessions generally (not only in-app browsers).
- Prompts are suppressed in standalone/installed PWA display mode.
- If not WhatsApp-verified, users see the Verify prompt.
- If WhatsApp-verified, users see the Add to Home Screen prompt and the linked profile can also retain the verified WhatsApp number returned by Lalo.
- After successful WhatsApp verification, a one-time install follow-up can appear.
- Banner dismissals are stored locally with a 7-day cooldown.
- If WhatsApp verification is disabled by environment flags, the Verify prompt can still appear, but its Verify CTA is disabled so guidance does not silently disappear.
- WhatsApp completion paths use one shared handoff (
src/integrations/lalo/completeWhatsAppAuth.ts) so install follow-up prompt logic is not duplicated per page.
Detection caveat:
- In-app-browser/webview detection still uses user-agent plus referrer heuristics for instruction tailoring, and remains intentionally best-effort. The prompts themselves are non-blocking and fully dismissible.
The app includes a Supabase-backed in-app notification system for signed-in users.
- Notification UX is mounted in the main top header (
AppTopBar) with bell + unread badge (capped at9+), bottom-sheet inbox, and detail modal. - Client state and live updates are handled in
src/hooks/useNotifications.ts. - Notification UI components live in
src/components/system/NotificationBell.tsx,src/components/system/NotificationsSheet.tsx, andsrc/components/system/NotificationDetailModal.tsx. - Notification schema, RLS, host-send RPCs, and automatic notification triggers are defined in
supabase/migrations/20260406113000_add_in_app_notifications.sql.
Current behavior:
- Users can only read or mark read their own notifications.
- Hosts can send notifications to valid, event-related users from
HostDashboard. - Guests can reply to eligible host-sent activity messages, which creates
guest_replynotifications for hosts throughreply_to_event_hosts(...). - Host-facing notification flows now work alongside richer WhatsApp-number visibility in host dashboard recipient/contact surfaces when that profile data exists.
- Hosts also receive
host_joinnotifications when people newly join, request to join, or enter the waitlist for a hosted activity. - Rapid same-actor joins for the same activity are batched into one delayed host notification listing all collected names instead of generating one host alert per attendee insert.
- Automatic notifications are generated for:
- activity shared
- important activity updates (title/time/timezone/duration/location/description/public summary)
- waitlist and attendance status transitions
- Host-facing activity notifications should deep-link to
/host/events/:id, while attendee-facing activity notifications should deep-link to/events/:slug. - Realtime updates are used for inbox/unread count while the app is open.
The app includes a push-notification delivery pipeline for installed PWA usage.
- Push controls live in
src/pages/ProfileSettings.tsx. - Push is only available when:
- the app is opened in installed/standalone mode
- the signed-in profile has
attendee_profiles.lalo_user_id
- Browser subscriptions and push category preferences are stored in:
public.push_subscriptionspublic.notification_preferences
- New rows in
public.notificationsare queued for push viapublic.push_dispatch_queue. - Delivery is handled by the
push-dispatchSupabase Edge Function. - Push notification clicks now preserve stored activity-specific action URLs when available instead of defaulting all activity notifications to
/.
Operational note:
- Run
push-dispatchon an interval (cron/scheduler) withx-push-dispatch-secretso pending queue rows are delivered.
- Frontend: React 19 + TypeScript + Vite 6
- Styling: Tailwind CSS v4
- Routing:
react-router-dom - Motion/animation:
motion - Backend/data/auth: Supabase (Auth, Postgres, Realtime)
- Utilities:
clsx,tailwind-merge,lucide-react
There is no Airtable integration and no first-party backend server in this repository.
Important areas:
src/App.tsx: auth bootstrap and route tablesrc/pages/: page-level UI and flow logicsrc/services/guestService.ts: guest identity and profile syncsrc/lib/: shared helpers for events, navigation, interests, RSVP, bookings, attendees, auth redirects, and modal/body-scroll behaviorsrc/utils.ts: shared formatting, timezone, slug, and calendar helperssupabase_schema.sql: baseline schema snapshotsupabase_reconcile_live_schema.sql: reconciliation/RPC-heavy SQL for live environmentssupabase_guest_identity_migration.sql: guest/session bootstrap schema
Identity-sensitive SQL:
merge_attendee_profiles(...): canonical guest/profile merge RPC used when a no-email or guest profile is upgraded into an existing email-backed identity
Required:
VITE_SUPABASE_URLVITE_SUPABASE_ANON_KEY
Recommended:
VITE_APP_URL- required for hosted/proxied auth redirects outside localhost
- keeps magic-link and account-merge returns pinned to the canonical app origin
VITE_MODERATION_ADMIN_EMAILS- comma-separated allowlist for the hidden moderation page at
/admin/moderation - only affects whether the page is shown/usable in the frontend
- comma-separated allowlist for the hidden moderation page at
VITE_FEEDBACK_ADMIN_EMAILS- optional comma-separated allowlist for
/adminand/admin/feedback - can be omitted if you want to rely on the moderation frontend allowlist instead
- optional comma-separated allowlist for
VITE_WEB_PUSH_PUBLIC_KEY- VAPID public key used by browser
PushManager.subscribe
- VAPID public key used by browser
Edge function runtime:
SUPABASE_SERVICE_ROLE_KEYSUPABASE_SERVICE_ROLE_KEYis provided automatically by Supabase Edge Functions and should not be manually set withsupabase secrets setOPENAI_API_KEYWEB_PUSH_PUBLIC_KEYWEB_PUSH_PRIVATE_KEYPUSH_DISPATCH_SECRET
Optional edge function runtime:
OPENAI_MODERATION_MODEL- defaults to
gpt-5.4-nano
- defaults to
OPENAI_FEEDBACK_MODEL- model for abuse-only filtering in the feedback intake function
OPENAI_API_BASE_URL- for OpenAI-compatible endpoints if needed
MODERATION_ADMIN_EMAILS- comma-separated allowlist for server-authorized manual moderation overrides
FEEDBACK_ADMIN_EMAILS- optional allowlist for manual
trello-prompt-syncruns; falls back toMODERATION_ADMIN_EMAILSwhen unset
- optional allowlist for manual
TRELLO_API_KEYTRELLO_API_TOKENTRELLO_INTAKE_LIST_ID- Trello list that receives sanitized cards from in-app feedback submissions
TRELLO_PROMPT_TRIGGER_LIST_ID- moving any card into this list triggers Codex prompt generation
TRELLO_API_SECRET- Trello app secret used to verify
x-trello-webhooksignatures
- Trello app secret used to verify
LALO_ENGINEERING_INTERNAL_API_KEY- internal key used by
trello-prompt-syncto call Lalo feedback prompt API
- internal key used by
ENGINEERING_SERVICE_BASE_URL(preferred)LALO_ENGINEERING_API_BASE_URL(legacy fallback)- base URL for Lalo internal engineering API (required in hosted environments)
LALO_ENGINEERING_APP- app identifier sent to Lalo (
im_inby default)
- app identifier sent to Lalo (
TRELLO_WEBHOOK_CALLBACK_URL- optional explicit callback URL used when verifying Trello signatures (must match Trello webhook callbackURL exactly if set)
FEEDBACK_SCREENSHOT_BUCKET- defaults to
feedback-screenshots
- defaults to
WEB_PUSH_SUBJECT- optional VAPID subject, defaults to
mailto:support@im-in.local
- optional VAPID subject, defaults to
Legacy optional:
APP_URL
See .env.example for the checked-in template.
Prerequisite: Node.js 20+ is recommended.
- Install dependencies with
npm install - Copy
.env.exampleto.env.localor.env - Set
VITE_SUPABASE_URL - Set
VITE_SUPABASE_ANON_KEY - Set
VITE_APP_URLif you want explicit hosted redirect behavior locally or in a proxy setup - Run
npm run dev
If you want the AI moderation flow to work locally as well, also configure the Edge Function runtime with:
SUPABASE_SERVICE_ROLE_KEYOPENAI_API_KEY
Changelog layman-summary generation:
npm run devandnpm run buildnow runscripts/generate-changelog-summary.mjsbefore Vite starts/builds- the script writes
src/generated/changelogSummary.ts - if
OPENAI_API_KEYis set, it attempts AI rewriting withOPENAI_CHANGELOG_MODEL(default:gpt-4.1-mini) - if AI credentials are missing or the API call fails, it falls back to deterministic non-AI summaries so builds still succeed
- the website never calls AI when users open
/changelog; page views only read the generated static artifact
Default local URL:
http://localhost:3000
npm run devnpm run buildnpm run previewnpm run lintnpm run clean
Notes:
npm run lintis currently a TypeScript no-emit check, not ESLint- there is no automated test suite in the repo today
- meaningful changes should be smoke-tested manually
- changelog layman summaries are generated once per prebuild/predev run, not per page view
Important app routes:
//my-activities/login/create-event/host/events/:id/edit/events/:slug/host/events/:id/calendar/moderation/admin/admin/moderation/admin/feedback/bookings/recover
Important route behavior:
/is the public home page for both signed-out and signed-in users/my-activitiesis the signed-in dashboard for hosting and attending/host/events/:idand/host/events/:id/editrequire a signed-in user/moderationis public-facing and only shows moderation history for public content/adminis the hidden landing page for admin tools/admin/moderationis a hidden allowlist-gated admin page layered on top of normal auth/admin/feedbackis a hidden allowlist-gated admin page for internal feedback review/bookingsis guest-session driven, not the main authenticated dashboard- unknown routes redirect to
/
Global menu behavior:
- the hamburger menu now comes from one shared component across top-bar routes and attendee activity detail headers
Create ActivityandExplore Activitiesappear at the very top of the hamburger menu list, with icons, across all menu entry points- attendee activity detail headers pair icon-only share with the shared hamburger menu control for compact right-side actions
Feedback entry behavior:
- the public home page has a
Send feedbackmodal available to signed-out and signed-in users - a global floating feedback button is available across app routes and opens the same feedback modal
- on attendee activity detail pages (
/events/:slug), that floating button is repositioned to the top-right below the sticky nav bar - on all other routes, the floating button keeps its existing bottom-floating placement
- feedback supports
bug,feature, andfeedbacktypes, plus optional screenshot upload - submissions run a lightweight abuse filter first, then create a sanitized Trello card in the configured intake list
- the success state now also links people to the public dev board so they can see what is being worked on
- Codex prompt generation is intentionally separate and only runs when a Trello card is moved to the configured prompt-trigger list
- prompt generation now creates a platform-owned engineering work item through Lalo internal API (
POST /api/platform/internal/engineering-worker/work-items) and uses canonical reusable rules fromlalo-platform/docs/ai/* - the recommended production setup is a Trello board webhook pointed at
trello-prompt-sync - webhook-triggered runs verify Trello-native
x-trello-webhooksignatures usingTRELLO_API_SECRET - a manual admin fallback still exists via
{"syncFromTriggerList": true}when webhook setup is unavailable - internal review for blocked/failed/not-sent feedback now lives at
/admin/feedback /admin/feedbacknow includesReview,Passed,Blocked,Failed,Archived, andAllbuckets- feedback records can be permanently deleted from
/admin/feedback, with typedDELETEconfirmation in the UI
The frontend relies on these important RPCs in supabase_reconcile_live_schema.sql:
submit_rsvp(...)request_or_submit_rsvp(...)get_my_join_request_for_event(...)list_event_join_requests_for_host(...)approve_event_join_request(...)reject_event_join_request(...)cancel_attendee_with_promotion(...)add_proxy_attendee(...)toggle_event_interest(...)
The app also relies on these core tables:
eventsevent_attendeesevent_hostsevent_interestsevent_access_requestsevent_join_requestsattendee_profilesattendee_sessionsfeedback_submissionstrello_prompt_jobs
If join requests fail with event_attendees_status_check errors about pending_approval, run this in Supabase SQL editor:
DO $$
DECLARE
c RECORD;
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conrelid = 'public.event_attendees'::regclass
AND conname = 'event_attendees_status_check'
) THEN
ALTER TABLE public.event_attendees
DROP CONSTRAINT event_attendees_status_check;
END IF;
FOR c IN
SELECT conname
FROM pg_constraint
WHERE conrelid = 'public.event_attendees'::regclass
AND contype = 'c'
AND conname <> 'event_attendees_status_check'
AND pg_get_constraintdef(oid) ILIKE '%status%'
LOOP
EXECUTE format('ALTER TABLE public.event_attendees DROP CONSTRAINT %I', c.conname);
END LOOP;
ALTER TABLE public.event_attendees
ADD CONSTRAINT event_attendees_status_check
CHECK (status IN ('confirmed', 'waitlist', 'pending_approval', 'cancelled'));
END $$;This hotfix is safe to run multiple times and is already included in supabase_reconcile_live_schema.sql.
This app is suitable for static deployment on platforms like Render, Cloudflare Pages, or similar static hosts.
Build settings:
- Build command:
npm run build - Publish directory:
dist
Required build-time env vars:
VITE_SUPABASE_URLVITE_SUPABASE_ANON_KEY
Required hosted env var:
VITE_APP_URL
Hosted auth flows now fail closed without VITE_APP_URL so sign-in and recovery do not silently bounce to the wrong origin. Because this is a SPA using BrowserRouter, configure rewrites/fallback to index.html so deep links continue to work.
Deep links that need rewrite support:
/events/:slug/host/events/:id/host/events/:id/edit/calendar/bookings/recover
For automatic Codex prompt generation, create a Trello webhook on the board and point it at:
https://<your-project-ref>.functions.supabase.co/trello-prompt-sync
The function filters board events internally and only reacts when a card is moved into TRELLO_PROMPT_TRIGGER_LIST_ID.
Configure a Trello webhook callback URL that exactly matches your deployed endpoint URL. Signature verification uses the raw request body plus callback URL and TRELLO_API_SECRET.
supabase_schema.sqlis not a guaranteed production snapshot by itself- schema and behavior are split across
supabase_schema.sql,supabase_reconcile_live_schema.sql,supabase_guest_identity_migration.sql, andSCHEMA_ALIGNMENT.md - guest recovery messaging and implementation are not perfectly aligned
/bookingsis guest-session-centric, while signed-in users mainly use Home- waitlist behavior depends on a mix of client helpers, SQL triggers, and RPCs
- some identity/name resolution logic remains more complex than it should be
- no automated tests currently protect the high-risk flows
See CONTRIBUTING.md
See GOVERNANCE.md
See PRIVACY.md
See TERMS.md
See LICENSING.md
The product is positioned as an open, community-built project. The landing page currently links to:
- roadmap: Trello board
- contact:
hello@joinimin.com - explainer modals:
Why this existsandHelp build it
PROJECT_ARCHITECTURE.md: current architecture and flow-level explanationFEATURES.md: feature-by-feature product referenceCURRENT_STATE.md: truth-on-the-ground implementation and gapsCONTRIBUTING.md: contributor guidance and safety notesSCHEMA_OR_DATA_MODEL.md: data model and identity/schema assumptionsAI_MODERATION.md: moderation triggers, stored fields, prompt, and cost-control strategyAI_MODERATION.md: public-facing moderation boundaries, transparency log, prompt, and privacy rulesFEEDBACK_PIPELINE.md: feedback intake, abuse filtering, Trello sync, and Trello-triggered Codex prompt generationCHANGELOG.md: human-readable summary of notable product and technical changesAI_DEV_RULES.md: implementation safety guidanceAUTH_UNIFICATION_PLAN.md: future-facing auth unification planSCHEMA_ALIGNMENT.md: schema drift notesRELEASE_CHECKLIST.md: release smoke-check list
Admin rule:
- any new hidden
/admin/*feature should also be linked from/adminso there is one discoverable entry point for internal tooling