System architecture and boundaries
The architecture
Wingman coordinates temporary, platonic meetups without a permanent account or inbox. A request is an availability record describing a person’s plan, proposed place, time and travel radius. The central design choice is to keep shared decisions on the server while the browser retains enough local state to make editing and short interruptions manageable.
Diagram
Presence · caches · leases
Access through photo API
Overpass map queries
Text version
- Browser: forms, radar, local recovery and tab signals send HTTPS actions and polls to the application API and receive authoritative snapshots.
- Cloudflare Worker: resolves session ownership, checks inputs and applies shared-state transitions.
- D1: stores requests, matches, presence, caches and coordination leases.
- R2: stores images and thumbnails, accessed through the photo API.
- Providers: Photon supplies geocoding and Overpass supplies map queries. The browser separately loads OpenStreetMap tiles and an approximate IP lookup.
| Component | Owns | Boundary |
|---|---|---|
| React / TypeScript browser | Editing, maps, local convenience/recovery state and display of server snapshots. | A locally displayed request is not proof of a successful shared-state write. |
| Cloudflare Worker API | Validation, session resolution, renewals, matching rules, photo access and snapshot construction. | All shared mutations go through application routes. |
| D1 (SQLite) | Live records, memberships, operational metadata, lookup caches and coordination leases. | Database constraints and conditional updates protect shared transitions. |
| R2 | Uploaded photos and smaller display versions. | Application endpoints check availability and access before serving images. |
The application uses Vinext with Vite and a Next.js-style route structure. The rest of this document follows the request lifecycle: establishing a session, discovering and agreeing on a place, preserving or retiring data, then recovering from operational failures. Revision links are grouped in collapsed blocks at the bottom of their sections so implementation history can be followed without interrupting the main explanation.
Revision history and change links
- Core service launched in v1.
Identity, presence and synchronization
How it works without a login
There is no username, password, email verification or account-registration flow. On initialization, the page generates 32 cryptographically random bytes with crypto.getRandomValues, represented as a 64-character hexadecimal token. That token supplies a temporary session capability when the browser does not already have a valid session cookie.
Capability-based request ownership
The server prefers the wingman-session cookie over the token supplied in a request body. It computes a SHA-256 hash of the selected token and uses that hash to look up the browser’s live request. The database stores the hash, not the original session token. Possessing the session capability lets a browser manage that request; the displayed name is not a credential.
When a live request exists, responses set an HttpOnly, SameSite=Strict cookie with a 90-second lifetime. Production HTTPS responses also set Secure. Successful activity renews the cookie. The HttpOnly cookie is not readable by ordinary page JavaScript, although the newly generated fallback token exists in page memory. When no live request remains, the API clears the cookie.
Reload and recovery boundaries
This lets a reload recover a still-live request without asking the person to log in. A random UUID identifies the public request separately from the private session capability. Database uniqueness on the token hash prevents multiple request rows for the same token. An expired request is not a recoverable account: saved form details can help post a new request, but do not recreate the expired server record.
Login-free does not mean anonymous or identity-verified. Public requests contain the information the user enters, and infrastructure receives network information such as IP addresses. Names, ages, photos and intentions are not independently verified. There is no cross-device account recovery or identity-based “one person, one request” guarantee.
Synchronization and heartbeats
Shared state follows a request–snapshot protocol. A browser sends an action to /api/wingman; the Worker checks the session, applies any permitted change, and returns the current view of the request, match, radar and counts. A successful mutation therefore updates the initiating page immediately, while another visitor normally observes it on their next successful poll.
Routine state polls target a ten-second interval. Each successful renewal moves the live request’s expiry to 90 seconds after server time. This margin allows brief interruptions without turning browser timers into the authority for availability. The transport is HTTP polling: a suspended browser cannot rely on receiving a change or renewing its request.
Sequence · Renewal and shared snapshots
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Browser A → Worker / D1: Send state poll or explicit action
- Resolve session; renew a live request. Renew visitor presence only when supplied by a visible homepage.
- Worker / D1 → Browser A: Return current request, match and counts
- Browser B → Worker / D1: Send next state poll (about every 10 s)
- Worker / D1 → Browser B: Return the latest shared snapshot
- A timeout or suspended tab delays observation. Later checks reconcile with server state.
Three clocks with different meanings
The interface separates the age of a request from server renewal and browser interaction. Their labels answer different questions, so none should be used as a substitute for the others.
| Indicator | Derived from | Interpretation |
|---|---|---|
| Request age | Original request creation time; locally refreshed against the latest server-clock observation. | Editing and renewal do not reset age. Reposting starts a new request. |
| Last check-in | Observed lease expiry minus the 90-second lifetime. | Delayed after 30 seconds; possible-expiry warning after 60 seconds. At the deadline, await a new server observation. |
| Last browser activity | Newest browser-observed interaction timestamp accepted for that live request. | Approximate interaction age, not proof of attention; does not itself renew the lease. |
The activity observer retains one page-memory timestamp for trusted pointer, wheel, touch and keyboard interaction, including scrolling near trusted input. It does not retain event contents, coordinates or a history, and ignores synthetic events, hidden-page events, visibility changes and automatic scrolling alone. Existing live-request/create/wingfam calls carry elapsed milliseconds; there is no event-triggered network call. The server bounds the age to seven days and ignores older same-request reports from other tabs.
Missing activity observations leave the stored value unchanged. Old clients show Not observed; missing clock values show Unavailable. Saved history retains original request age but no ongoing heartbeat or activity metadata. The request’s activity field is removed with that request, although an already received snapshot or temporary final-match copy can retain it.
Concurrency and recovery
Page-level guards prevent overlapping routine polls and avoid applying a poll during a mutation. Matching calls time out after 12 seconds; failures and an aging last-success timestamp put the interface into a connection-problem state. Later checks, reconnection and page-return events refresh it. Map movement and radius changes ride the next routine poll, while explicit actions and cross-tab signals can trigger additional refreshes.
Hidden pages can continue request heartbeats even though they omit visitor renewal. Closing or suspending a page is therefore different from explicit withdrawal: another tab may still maintain the request, and the server lease remains the final authority.
Multiple tabs in one browser
Ordinary tabs in the same browser profile share the site’s cookies and local storage. After a successful modifying action, Wingman writes a small change signal to local storage. Other tabs receive the browser’s storage event and schedule a server refresh. The signal contains timing information and a random marker, not a copy of someone’s request or photo.
A separate posting marker lasts 15 seconds and discourages another tab from starting a simultaneous new request. A tab only releases a marker it owns; an abandoned marker expires. This is deliberately lightweight coordination. Local storage does not provide an atomic distributed lock, simultaneous races are still possible, and unavailable storage does not prevent basic use. Separate browser profiles, devices and private-browsing contexts are not coordinated by this mechanism.
Each tab still keeps its own editing controls. The shared live request is synchronized through server snapshots; this is not collaborative character-by-character form editing. The database does not store a permanent browser identity for tab coordination.
Revision history and change links
- Introduced in v31.
Worldwide visitor presence
Presence measures browsers recently seen by the service. It supports a worldwide summary and nearby discovery without requiring someone to post a request. The design intentionally keeps this measure separate from the number of live requests: a browser with a request can appear in both populations.
| Count | Population | Boundary |
|---|---|---|
| Worldwide requests | Live request rows. | Does not include visitors who are only browsing. |
| Worldwide viewers | Unexpired browser-presence records, including request owners. | A 90-second observation window; not an exact count of people looking at a screen. |
| Nearby viewers without requests | Located presence records in the search area with no associated live request. | Includes the viewing browser when eligible; returns an aggregate, not individual locations. |
Presence renewal and location
A visible homepage supplies a random UUID with its existing API calls. Ordinary tabs share that UUID and a 90-second reuse deadline in local storage under wingman-viewer. The server hashes the identifier and upserts a D1 row containing expiry, available location and a temporary request-session association. This normally deduplicates coordinated tabs. With unavailable storage, a page-memory identifier remains stable only within that tab.
The request session and visitor identifier have different jobs. A hidden page omits visitor renewal even if it continues maintaining a request. Expired presence rows stop counting immediately and are physically removed on later API activity. A later visible visit replaces an expired local identifier; an inactive browser’s storage is not erased by a timer.
Without a live request, discovery uses the selected meetup point, then the device location, then the approximate IP area. Device detection can refresh discovery even when that point is unsuitable for a meetup. Nearby visitor counting uses the same great-circle distance and search area as requests, but returns only an aggregate. Coordinates are client-supplied, and neither the count nor a location label verifies physical presence.
City resolution and aggregation
The worldwide viewer number opens a city breakdown. When device coordinates are available, the server rounds them to a 0.001-degree cell (about 100 metres at the equator) and asks Photon for a city label. Genuine city, town or village fields and city-layer names qualify; districts and subdivisions do not become cities. If the first response has no usable label, a second query considers up to five city-layer features within 20 km and accepts the first matching the known country and state/province.
This produces an approximate nearby municipality/community label. Missing or failed device results fall back to Cloudflare’s IP geography; approximate IP coordinates can also be reverse-geocoded when hosting city labels are absent. The server ignores geographic labels supplied in the request body. Mobile networks, VPNs and incomplete map data can make fallback labels inaccurate, so missing results remain unavailable.
| Concern | Implemented behavior |
|---|---|
| Label cache | Rule-versioned rounded-coordinate keys in D1; successes 24 hours, negative results 30 seconds. Worker memory: up to 500 entries, at most five minutes. Shared in-flight work and a six-second uncached lookup budget. |
| City ordering | Server groups active viewers by city, region and country; returns up to 50 groups per order plus other/unknown totals. Most viewers uses count then alphabetical ties. Nearest cities uses distance, unknown distances last. |
| City distance | Straight-line distance from the supplied device/IP origin to a public representative city point. It is not distance to another viewer or to the selected meetup pin. |
| Popover layout | Shows up to eight rows as height allows. Matching country and region labels can be omitted visually while the group identity remains complete. The chosen sort is kept in page memory. |
Public city-center lookups use Photon searches constrained by the city, state/province and country identity. Only matching city-layer point results are accepted.
Their geographic-name keys store public coordinates and expiry, with 24-hour successful reuse and 30-second negative reuse. A three-second provider timeout and shared same-city in-flight work bound lookup effort; shared cleanup removes expired rows. No visitor identifiers are stored in that cache. Missing origins disable distance sorting, and missing city points show Distance unavailable. Distances use browser-locale measurement conventions, including an explicit measurement-system extension when supplied.
Presence labels expire with the visitor row; the reusable geography caches are independent of visitor identifiers. Separate devices/profiles count separately, shared browsers can represent several people, first-visit races can briefly duplicate identifiers, and automated clients can inflate counts. The service does not fingerprint visitors or deduplicate by IP address.
Revision history and change links
Discovery and meetup agreement
Maps, location and business search
Location is a staged workflow: find a browsing origin, discover a candidate meeting place, validate that candidate, then publish it as a fixed point. Keeping these stages separate lets someone browse from home or recover from a location failure without silently advertising that location as a meetup.
| Value | What updates it | What it controls |
|---|---|---|
| Device estimate | Browser geolocation at startup and a watch while visible. A failed later reading retains the last precise coordinates in page memory. | Current-location marker, distance estimates and automatic suitability/business preparation. |
| Approximate IP area | Session-cached result or api.ipapi.is before a precise fix is available. | Fallback browsing origin and approximate-area display; never a replacement for an already acquired precise fix. |
| Selected meetup point | An explicit eligible address, business, saved location or map-pin selection. | The fixed public meeting address and coordinates when a request is posted. |
| Map viewport | Saved center/zoom, manual gestures, explicit selection and defined automatic framing. | Presentation only; moving or zooming the map does not change the meetup point. |
Device acquisition and map presentation
The geolocation watch stops while hidden and restarts on return. The browser and operating system determine delivery frequency; reported accuracy is an estimate. Once a precise reading has arrived, a later failure retains it and displays “precise location unavailable - using previously obtained location.” A fresh success clears the warning. A delayed IP lookup cannot replace a precise fix that arrived while it was pending.
Leaflet renders OpenStreetMap tiles, the search radius and interactive request markers. A blue device dot and accuracy halo remain visible at every zoom when their coordinates are inside the viewport, even if suitability is unknown or unsuitable. IP-only positioning uses a 30-mile approximate-area overlay instead. Search-radius and approximate-area shading use clipped 360-point geodesic polygons; distance eligibility still uses its own great-circle calculation.
| State or event | Framing behavior |
|---|---|
| No selected point; first precise fix | Overrides a saved wide IP view and centers the device, including when the fix precedes map initialization. |
| Current point allowed | Opens a close view up to zoom 17; widens for the accuracy halo and tightens as accuracy improves. Movement beyond 5 metres controls following. |
| Current check pending, unknown or failed | Uses the radius view until an allowed result has been observed; thereafter preserves the close view. Only a confirmed unsuitable result returns automatic browsing to the radius view. |
| Explicit address/business/history selection | Centers the checked pin at zoom 17. Later pre-posting device readings do not move or refocus that pin. |
| Double-click or double-tap pin drop | Checks the point and preserves map center, zoom and page position. A failed replacement preserves the prior selection. |
| Active request with device coordinates | Fits only the meetup and latest device point with 30-pixel padding, up to zoom 17. Fits immediately on activation/first fix, then at most every ten seconds with latest coordinates. |
| Active request without device coordinates | Keeps the meeting-pin view. IP coordinates are not used as the device endpoint. Withdrawal or loss of the request cancels pending fits. |
The saved main-map viewport is validated before restoration; storage failure does not block the map. Compact match maps use zoom 17. Active-request fits tighten as the device approaches the selected meetup point, leave manual pan/zoom available between updates, and do nothing when coordinates are unchanged. Fractional zoom reduces spare space on narrow screens. In the radius browsing view, routine device updates preserve the viewport; accuracy-only changes in the close view adjust zoom without deliberate recentering.
A five-second acquisition notice appears when a loaded map first receives fresh device coordinates or recovers from failure. Periodic successful readings do not restart it; selecting a meetup or showing the retained-location warning hides it. The address and distance sit above the map, and the legend identifies pins, requests and the device marker. The radius control supplies the shading’s distance. It spans ¼–50 miles on a gentle, shifted logarithmic scale, giving finer control nearby and gradually larger increases toward the upper end. Slider selections snap to ¼-mile increments through 5 miles, ½-mile increments above 5 through 10 miles, 1-mile increments above 10 through 20 miles, and 5-mile increments above 20 through 50 miles. Arrow keys move to the next or previous allowed distance, including across band boundaries; Page Up and Page Down move ten allowed steps, and Home and End select the limits. The editable number field stays synchronized in miles. A live match disables both controls.
Nearby business selection
The dropdown is a user-activated discovery tool. Its center is the selected meeting point when present, otherwise the detected browsing origin. Distance choices are constrained by the search radius; the control asks for at least a one-mile radius when no business-distance option is available. Address and business-name searches use Photon, while nearby lists use Overpass queries over OpenStreetMap data.
Sequence · Discovering and selecting a nearby business
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Browser → Worker / caches: Open list or change distance: request businesses
- Use a fresh raw-query result, or coordinate one provider lookup for the complete query key.
- Worker / caches → Map providers: On cache miss, query mapped businesses
- Map providers → Worker / caches: Return complete map elements
- Remove duplicates, private/adult venues and out-of-radius results; sort nearest first, limit to 30.
- Worker / caches → Browser: Return choices; an empty list is valid
- Browser → Worker / caches: User chooses a business: check exact point
- Worker / caches → Browser: Return eligibility decision or unavailable
- Allowed: fix the selected pin and focus it. Failed manual replacement: preserve the previous pin.
The browser’s list cache lasts two minutes, uses the center rounded to five decimal places plus distance, and clears when it reaches ten entries. Opening a stale list, changing distance or changing center after activation can fetch again. A failed lookup clears the list, closes it and schedules another attempt about every two minutes while enabled; the browser can delay that retry. Address search and manual pin selection remain alternatives.
Adult-entertainment categories and recognizable names are filtered from business/address results and obvious submitted addresses. Incomplete tags or neutral names can evade that screen. A candidate’s exact coordinates still pass the independent suitability check before becoming a meeting point.
Preparing “Use my location”
This button selects a prepared mapped business; it does not use a raw device coordinate as an automatic meetup. Current-location suitability samples the latest device point initially and about every 30 seconds while visible. Only a current allowed result enables automatic business preparation. Unsuitable, checking, unknown and unavailable states suppress preparation but do not disable explicit discovery tools.
Sequence · Preparing a business before the click
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Gate: precise or retained device coordinates, allowed current-location check, no active match or locked wingfam.
- Browser → Current-business API: Sample latest point; prepare nearby business
- Query within a fixed 50 metres. Try only the nearest candidate, not every listed business.
- Current-business API → Suitability service: Check associated public main entrance if present
- Suitability service → Current-business API: Allowed + business, or try mapped center
- Current-business API → Suitability service: If needed, check candidate node / center
- Suitability service → Current-business API: Return allowed + business, rejection or error
- Current-business API → Browser: Return checked selection or no ready business
- Only a later user click copies the prepared selection to the fixed meetup pin; the click starts no lookup.
For a mapped way, an associated public entrance=main within the same radius is considered before its center. A checked point must be both allowed and identified as a business. A no-business result or failure leaves the button disabled; a delayed response never selects a pin automatically. A ready result can still be used with retained precise coordinates, with the stale-location warning visible.
The latest current-location classification and prepared business/no-business result each have a page-memory reuse window of up to 24 hours within 5 metres of their lookup origin. Movement beyond that tolerance or expiry causes work at a later sampling step. Failures wait at least 30 seconds before retry on a later device update; obsolete requests are cancelled. Active matches and locked wingfams suspend this work. This tolerance is for presentation and reuse of a checked canonical business point, not approval spreading to nearby raw coordinates.
Current-location notices appear beside the controls only while no meeting point is selected. Once a point is selected or restored, its own status takes that position. Diagnostics keep the separate results available. Pending replacement preparation does not keep showing an old business as newly ready.
Meeting-point suitability
The server validates coordinates when creating or updating a request and when reopening a wingfam. Selection controls also call /api/meetup-location before accepting a new point. Restored draft and history coordinates are checked again; a saved address does not restore an approval.
Sequence · Acquiring evidence for a suitability decision
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Browser → Worker / D1: Check exact selected coordinates
- Fresh exact-point cache hit: return its decision. Otherwise share local work and acquire / wait on the D1 lookup lease.
- Lease owner rechecks cache, then reuses suitable nearby geometry if available; classify this point independently.
- Worker / D1 → Overpass providers: If geometry is unavailable, query mapped areas
- Fallbacks start after 1 s and 2 s, or sooner after failure; cancel remaining work on a complete response.
- Overpass providers → Worker / D1: Complete geometry, or provider failure
- Complete data: classify and cache allowed/rejected result. Transport error or partial response: do not cache a decision.
- Worker / D1 → Browser: Return decision + provenance, or unavailable
- Manual replacement failure keeps the old pin. Failed restored-point validation keeps the form locked.
Decision flow · Classifying one coordinate
Start only after complete map data is available. Evaluate these tests in order; an outcome ends classification.
Diagram
- 1Adult venue, residential building or private access at the point?Yes → RejectNo → Check the public venue
- 2Named eligible public-facing venue at the point?Yes → Allow; determine business flagNo → Check the containing land use
- 3Unsuitable land use, military area or water?Yes → RejectNo → Check commercial classification
- 4Mapped commercial or retail area or building?Yes → Allow (business flag not implied)No → Reject as unconfirmed
Text version
- Adult venue, residential building or private access at the point?
Yes → Reject. No → Check the public venue. - Named eligible public-facing venue at the point?
Yes → Allow; determine business flag. No → Check the containing land use. - Unsuitable land use, military area or water?
Yes → Reject. No → Check commercial classification. - Mapped commercial or retail area or building?
Yes → Allow (business flag not implied). No → Reject as unconfirmed.
The classifier considers containing land use and footprints, access, adult-venue tags and eligible public-facing venues. A public venue represented as a node must be within about 15 metres; footprints use point-in-polygon checks. The business flag is narrower than general eligibility, which is why a commercial area alone cannot enable the front-door recognition option.
Until a point is selected and checked, the form is inert and an overlay directs the visitor to location controls. Saved-request selection remains available above it. Notices use neutral for work in progress, green for a completed ready/eligible result, yellow for an unsuitable or missing point or no suitable business, and red for an unavailable or failed check. Text carries the meaning independently of color. Selecting a point unlocks editing; it does not post a request.
Overpass requests use Private.coffee, overpass-api.de and VK Maps (maps.mail.ru), with a ten-second client timeout per provider; suitability queries permit eight seconds of server execution. HTTP errors, timeout remarks and incomplete data are not classification results. These checks send coordinates and query instructions, not names, photos or plan text. Eligibility is based on mapped data and can have false positives or negatives; it is not an inspection, opening-hours check or guarantee of safety.
Cache boundaries and coordinated lookup ownership
Caching reduces repeated provider work while preserving each decision’s spatial boundary. Positive reuse windows are deliberately finite, and a read never extends the original server expiry. The following layers answer different questions.
| Layer / key | Lifetime | Limits and reuse rules |
|---|---|---|
| Photon address/name search: normalized Unicode, case and whitespace; exact coordinates for reverse lookup | Nonempty results: 24 hours. Valid empty results: 30 seconds. | D1 plus up to 500 Worker entries. Preserve city names and the remaining query. A selected result still needs suitability validation. |
| Suitability: exact coordinates + rule version | Allowed and rejected results: 24 hours. | D1 plus up to 500 Worker entries. Never spread approval to a neighboring point. |
| Reusable map geometry: source point + rule version | 24 hours; derived decisions inherit source expiry. | D1 plus up to 100 Worker entries. Reuse within 5 metres only with supported closed-way boundaries; relevant relation geometry or missing boundaries prevents reuse. |
| Raw business data: hash of complete Overpass query | Complete results, including valid empty results: 24 hours. | D1 plus up to 100 Worker results. Coordinates, radius, filters and output shape distinguish queries. Dropdown and current-business preparation use separate query shapes. |
| Browser current-location / prepared-business result: lookup origin | Up to 24 hours within 5 metres; failures have a 30-second retry cooldown. | Latest result in page memory. No localStorage suitability cache; selected meetup points are validated independently. |
An uncached suitability, address or raw-business lookup acquires a 20-second D1 lease keyed to that query. The owner rechecks the cache and publishes only while its random owner token remains current and unexpired. Waiters recheck with delays from 150 milliseconds to one second plus up to 99 milliseconds of jitter, with a 12.5-second deadline. During their first 1.5 seconds they can acquire a released lease; afterwards they only await a result or return a retryable error.
The owner releases on failure without caching the error. A late owner cannot overwrite a replacement’s result or release its lease. A crashed owner stops blocking acquisition after expiry; coordination-storage failure falls back to per-Worker sharing and provider work. This reduces duplicate calls but does not guarantee exactly-once execution. A matching action’s shorter client timeout may end the browser wait while server work continues.
Provider errors and malformed/incomplete results are not cached. D1 caches have no separate row-eviction cap, but storage is finite. Expired entries are ignored and removed by shared background cleanup rather than a deletion timer. Cache failures can fall back to provider checks. Reusable coordinates can still describe sensitive places, and map data may remain outdated until expiry. Address/business HTTP responses use no-store instructions; browser-list and page-memory reuse are separate application caches.
Classification follows the provider’s Overpass query semantics and OpenStreetMap land-use tags.
Revision history and change links
- Address search and pins launched in v1.
- Nearby businesses added in v15.
- Radius-aware business distances added in v20.
- Logarithmic radius slider added in v92.
- Radius curve softened and quarter-mile snapping added in v94.
- Distance-dependent radius increments added in v95.
- Adult-venue screening added in v31.
- Map labels simplified in v67.
- Close-zoom rendering improved in v42.
- Precise-location initialization revised in v53.
- Independent device centering added in v54.
- Phone framing and acquisition notices refined in v55.
- Device-marker visibility and selection zoom updated in v62.
- Periodic device refresh introduced in v41.
- Continuous walking updates added in v55.
- Click-only business selection and retained-location recovery added in v52.
- Prepared business selection added in v55.
- Current-location notice visibility refined in v68.
- Shared suitability caching introduced in v48.
- 24-hour results, geometry reuse and earlier provider fallback added in v51.
- Meeting-point screening introduced in v34.
- The required-point form overlay was added in v39.
- Suitability notice styling added in v65.
- Provider recovery improved in v56.
- Cross-Worker deduplication added in v58.
- Shared text-search caching added in v56.
- Shared query caching and deduplication added in v58.
- Valid empty-result caching extended in v60.
Meeting times and travel radius
A request can express either a scheduled appointment or a rolling willingness to meet after a travel allowance. The distinction matters because discovery compares proposed times, while finalization must produce one fixed time that both participants can rely on reviewing.
| Mode | Stored meaning | Validation and finalization |
|---|---|---|
| Specify a time | Absolute timestamp, entered in 15-minute increments with AM/PM controls. | Must be between server now and 12 hours ahead. The browser chooses the next local occurrence of the clock time; there is no 6 AM cutoff. |
| Now / travel time | Reserved meet_at = 0 plus meet_delay_minutes. Now is zero; nonzero delays use five-minute increments. | Discovery evaluates now + delay. Finalization records a fixed final_meet_at; later heartbeats and phone updates preserve it. |
When both requests are relative, finalization uses the larger selected delay regardless of the chosen meeting location: 10 and 25 minutes produce a meeting 25 minutes after finalization. Two Now requests finalize for the current time. If only one request is relative, the selected meeting plan supplies the time. There is no separate grace allowance. A changed plan or withdrawn readiness clears the locked time so a later finalization can establish a new one.
Deriving a travel allowance
The duration dropdown precedes transportation. Choosing Now selects Already there, and Already there returns the duration to Now. Moving to a nonzero duration initially selects driving; walking, cycling and public transit are also available. The browser and posting API calculate the available range from straight-line distance between supplied detected coordinates and the meetup point.
| Mode | Lower estimate | Upper estimate |
|---|---|---|
| Walking | 20 | 25 |
| Cycling | 4 | 8 |
| Driving / rideshare | 1.5 | 5 |
| Public transit | 3 | 12 |
The lower estimate is rounded up to five minutes, then adds five minutes. The upper estimate is rounded up to five minutes with a ten-minute floor, then adds two five-minute increments. Both bounds are capped at 720 minutes. The dropdown offers Now and the resulting nonzero range; these heuristics shape the choices and add nothing to the delay after selection. They are not route calculations, traffic predictions or transit timetables.
Changing mode or meeting point can adjust an unsaved duration into range. A live request keeps its stored value until updated. The server rejects out-of-range nonzero submissions; wingfam reopening clamps a reused allowance for its confirmed point. Saved requests retain mode and delay. This calculation reuses coordinates already supplied for the 50-mile posting check and adds neither a routing provider nor a stored journey origin.
Expiry, automatic advancement and distance
Relative requests have no appointment-age expiry, but still expire after 90 seconds without server renewal. Scheduled appointments can optionally advance in 15-, 30-, 45- or 60-minute steps after their time passes. Server activity advances live requests; the browser can advance draft controls. Automatic advancement pauses during a wingmatch, and relative requests normalize that setting to zero.
Search radii range from a quarter mile to 50 miles. With unequal radii, one person may be able to travel to the other’s place even when the reverse is not allowed. Matching and finalization therefore validate the selected place rather than assuming both directions are interchangeable. Posting also requires the chosen point to be within 50 miles of the supplied detected location.
Form and overview layout
Required fields use an asterisk with a nearby legend: name, adult age, gender response, matching preference, plan, timing, meeting point and radius. Prefer not to say is a valid gender response. A photo and automatic advancement are optional. Browser controls provide immediate feedback; server schemas independently validate the submission.
- Age requests a whole-number numeric keypad, radius a decimal keypad, and phones a telephone keypad. The device decides the actual keyboard.
- Reset form remains centered below timing/transportation, with a separate gap before the compact Now / travel time explanation.
- The radius value and unit stay together. Above 800 pixels, the radius panel and worldwide summary use the same column proportions, 23-pixel gap and centered content width as the meeting-spot and request panels below. Their left edges align with those panels, and text wraps within its column.
- At 800 pixels and below, the overview retains its content-aware wrapping: the whole summary moves below the radius panel with a 20-pixel gap when both blocks cannot fit.
- Worldwide counts break after “and”. The reminder keeps “Stay here. Stay on the radar.” together, followed by separate lines for staying open and the 90-second reopening window; narrow layouts can wrap naturally.
Revision history and change links
- Relative timing added in v37.
- Timing layout refined in v68.
- The separate grace allowance was removed in v40.
- Control order and Already there added in v39.
- Distance-based starting durations and two extra upper choices added in v46.
- Required markers and Prefer not to say were added in v32.
- Mobile keypad hints added in v71.
- Radius control alignment refined in v77.
- Content-aware overview layout added in v80.
- Summary line breaks added in v81.
- Wide overview aligned with the panels below in v88.
- The former tomorrow-before-6-AM restriction was removed in v34.
- Auto-advance added in v10.
Matching and mutual agreement
Matching reserves two live requests while their owners agree on a place and a recognition plan. Discovery supplies candidates; the server independently checks compatibility and owns the state transitions. A request can participate in only one wingmatch at a time.
Compatibility and reservation
Compatibility requires mutual gender preferences, proposed meeting times within one hour, and at least one permitted meeting location. Relative times are evaluated as now plus the selected allowance. Distance uses a great-circle calculation, not a driving route. The “Looking for” preference supports comfort and personal safety in platonic company; each person must satisfy the other’s choice or the other must select Anyone.
A fresh/reset form defaults to Prefer not to say, while a valid saved response is restored. That response does not satisfy a specific-gender preference: the other visitor must choose Anyone. Self-reported gender does not verify identity or guarantee safety.
An invitation creates the match and both participant rows in a database batch. A unique participant key rejects competing reservations. At widths of at least 1,000 pixels, radar results occupy a scrollable region below the map beside the request panel; on smaller screens they follow the form in normal page flow. The heading remains visible and the results region is keyboard-focusable.
| State | Entry / permitted work | Failure or return path |
|---|---|---|
| Invited | The invitation reserves both requests; participants can accept or cancel. | Conflicting invitation is rejected. Cancellation releases participation. |
| Arranging | Both accept, agree on an allowed address and save recognition details. | A plan edit resets both ready flags and advances the plan revision. |
| One participant ready | Readiness refers to the reviewed plan revision. | A stale revision, invalid place or recognition conflict prevents readiness. |
| Final | Both ready for the same plan; server locks the meeting timestamp. | Completion removes ordinary matched requests. Expiry makes live interaction unavailable; temporary local recovery has separate limits. |
Meeting finalization and its view
Finalization is a mutual confirmation protocol. Before marking ready, the page saves pending recognition details. The server then checks that the submitted plan revision still matches, that both requests remain live, that the selected address is permitted and that recognition requirements are met. Conditional database updates prevent a stale screen from silently confirming a revised plan.
Sequence · Two participants finalize one plan
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Both accepted; both chose the same permitted meeting point.
- Participant A → Worker / D1: Save pending recognition details
- Worker / D1 → Participant A: Return current plan revision
- Participant A → Worker / D1: Ready: confirm reviewed plan revision
- Validate live participants, place and recognition. Reject stale revision or two “I’ll find you” choices.
- Worker / D1 → Participant A: Record A ready; wait for B
- Participant B → Worker / D1: Poll / review plan; confirm ready
- If plan details changed: clear both ready flags and require review again. Otherwise second readiness locks final_meet_at.
- Worker / D1 → Participant B: Return finalized match view
- Participant A → Worker / D1: Next successful state poll
- Worker / D1 → Participant A: Return same finalized plan and time
The finalized view presents the agreed place, locked time and recognition instructions. Its local recovery copy is separate from the live server match: it can preserve recently received details briefly after interruption, but cannot authorize a new server action or keep an expired match alive.
Recognition and optional contact exchange
Recognition mode is stored separately from text, with older records defaulting to a written description. Participants can describe themselves, choose I’ll find you, or choose Meet outside the front door. The latter requires a mapped named business at the agreed point; the browser checks before enabling it and the server checks again when saving or finalizing. A commercial district alone is insufficient, and map data does not identify a guaranteed accessible entrance.
The partner’s I’ll find you choice is visible before finalization, while private written recognition details remain for the final exchange. If both choose I’ll find you, both pages explain the conflict and the server refuses readiness until one changes their choice. Changing recognition mode or details resets both ready flags and increments the plan revision.
Phone exchange is optional. Numbers are parsed and validated, and the final view reveals them only when both people supply a number. Phone updates after finalization preserve the locked meeting time. The service processes these details; this is not end-to-end encrypted messaging.
Completion and wingfam continuation
Closing a finalized view records a local dismissal and requests completed-match cleanup. Ordinary matched requests are removed; an active wingfam host can remain available. The UI does not confuse deliberate completion or withdrawal with lease expiry.
A wingfam lets a participant reopen availability at the confirmed place. Reading the finalized snapshot renews a server permission for 90 seconds that preserves the relevant request information. Reopening requires explicit location confirmation, current eligibility or a valid cache result, and a point within 50 miles of the supplied browsing origin. The host’s address is fixed and guests must be able to travel to it.
Revision history and change links
- Responsive radar layout added in v81.
- Mutual matching launched in v1.
- Asymmetric travel support added in v4.
- Mutual gender preferences launched in v1.
- The default gender became “Prefer not to say” in v38.
- Recognition choices introduced in v34.
- The finalized-view phone exchange was added in v12.
- Wingfam reopening introduced in v16.
Data lifecycle and recovery
What is stored where
The storage design separates authoritative shared records from browser conveniences and reusable provider data. Expiring a live request ends its availability; it does not automatically erase every local draft or cached lookup related to the page.
| Layer | Contents | Lifecycle / access boundary |
|---|---|---|
| Page memory | Edits, snapshots, last interaction timestamp, diagnostics, provider logs, in-flight work, device readings, prepared business and fallback session token. | Lost on a full reload; not a durable account. |
| HttpOnly cookie | Temporary request-control capability. | 90-second live-request renewal; ordinary page JavaScript cannot read it. |
| Local storage | Draft/profile and processed photo; up to five previous requests; theme/sound/haptic preferences; viewport; visitor identifier; tab markers; final-match copies/dismissals; photo association. | Can outlive a live request and can be accessed by someone using the same browser profile. |
| Session storage | Approximate IP browsing area. | One-hour reuse window within the tab session; separate from server city caches. |
| D1 | Requests, token hashes, latest reported activity, matches, participants, visitor presence/geography/session association, rate limits, wingfam permissions, photo metadata/removal receipts, lookup caches and leases. | Server authority; each record category has its own expiry or cleanup behavior. |
| R2 | Photo originals and display variants. | Access through the photo endpoint; unused or removed files are subject to cleanup/retry. |
Local storage is not a private account vault. Clearing site data removes its conveniences; leaving them in place can retain information after a request ends. The Privacy Policy describes public fields, provider processing and retention in more detail.
Previous requests and finalized-copy recovery
Deduplication and draft reuse
Previous requests are parsed through the public request schema before being saved. This strips photos, session credentials, request IDs inside the saved form data, and private match details. The history is limited to five distinct entries. Requests whose saved fields all match except the meeting time or relative travel allowance are duplicates; photo differences do not count because photos are not stored in those entries. Other settings, including the auto-advance interval, remain part of the comparison.
Existing duplicates are cleaned up when history is read, retaining the most recently saved entry. Selecting an entry fills the draft, including the saved plan description, restores its saved meeting coordinates and address, refocuses the map, collapses the list and scrolls to the form. The saved meeting point is checked again; an eligible point unlocks the form and focuses the name field. If it is rejected or cannot be checked, the form stays locked and asks for another point. A delayed automatic location lookup cannot replace or clear an explicitly restored point. An out-of-range old scheduled meeting time moves to the next quarter hour. Relative requests retain their relative setting and travel allowance. Selection does not post anything; the user can customize the restored fields before submitting. The displayed previous date and time are italicized. Reset form stays centered below the timing controls, directly above the “Now” explanation when relative timing is selected. It clears the editable form and saved draft; it does not clear previous-request history or withdraw a live request.
Finalized view and persistent dismissal
A finalized match can have a separate local recovery copy with a 90-second expiry that is refreshed while its final view is open. Copies more than an hour past their fixed meeting time are not restored. Finalized relative plans use their locked timestamp for this check. The same 90-second copy expiry and dismissal checks apply. Closing the modal records a dismissal in local storage, honored for 24 hours, so a reload or another tab rewriting the copy does not reopen a dismissed match. Up to 30 dismissal markers are retained; expired ones are ignored and pruned on later dismissals. These markers contain match identifiers, not an account identity.
Live expiry is different from physical deletion
Live eligibility and physical deletion are separate stages. After 90 seconds without successful server renewal, a request no longer qualifies for live results. An already loaded page can continue showing its old snapshot until it refreshes. Explicit withdrawal instead requests immediate removal through the application.
Sequence · From live expiry to physical cleanup
Read from top to bottom. Dashed arrows are responses. Choose the diagram or its text version below. Wide diagrams keep sideways scrolling when fitting would make their labels too small.
Diagram
Text version
- Browser → Live API / D1: Renew a live request
- Live API / D1 → Browser: Expiry = server time + 90 seconds
- No renewal by deadline: live views exclude the request, even if its stored row has not been deleted.
- Browser → Live API / D1: Later qualifying API activity
- Live API / D1 → Browser: Return live snapshot without expired rows
- Live API / D1 → Background cleanup: After response, attempt shared cleanup lease
- One lease owner performs bounded row/file cleanup. Failure or backlog waits for later traffic.
Matching, meetup-check, business-search and place-search responses schedule cleanup using the hosting runtime’s after-response lifetime. Live database views already exclude expired requests, overdue scheduled plans and retired ordinary finalized pairs, so stale storage does not keep a request visible or busy. Eligible unmatched auto-advancing plans remain live until their due time adjustment is processed.
| Control | Implemented bound |
|---|---|
| Ownership | Atomic shared D1 lease; 120-second expiry, owner-checked release, 30-second successful-sweep cooldown and per-instance throttling. |
| Request batch | At most 200 expired requests per pass, plus retiring ordinary finalized partners and cascading dependent records. |
| Other expired records | Bounded batches of up to 200 per table for presence, rate limits, permissions, geography, city centers, address search and map/suitability caches; lookup leases also receive bounded cleanup. |
| Photo batch | Up to 20 unused assets, including their variants. Failed deletions can retry. |
| Time budget | Stop starting new stages after 20 seconds; the host can cancel unfinished work after its 30-second post-response allowance. |
Competing Workers skip a sweep when another owns the lease. Ownership checks stop an interrupted old job releasing a newer lease, and failed work is logged for retry on later traffic. Large backlogs take multiple passes; without qualifying traffic, expired data can remain stored. Deferred work still shares hosting/database resources, so it removes direct response waiting rather than all contention.
Correctness-critical operations remain in the live path: releasing an expired session slot before posting, releasing stale match membership before inviting, withdrawal, completion and visitor-requested photo removal. Cache reads/writes are also awaited. Backups, logs, removal receipts and browser copies have separate lifetimes; availability expiry is not a guarantee that every copy disappears.
Revision history and change links
- Background cleanup and a shared database lease added in v57.
Photo processing, access and visitor removal
Upload pipeline
The upload control accepts JPEG, PNG and WebP inputs up to 3.5 MB. The browser prepares a resized JPEG and smaller display copies; this is format/size processing, not image-content moderation. The server validates the supplied image data, checks dimensions and enforces a maximum 800-pixel image dimension and 250 KB processed-image size. Re-encoding and quality reduction are used when needed. Small and medium thumbnails are validated separately, with 160- and 400-pixel bounds.
Serving and access checks
Images use opaque photo IDs and are served through /api/photos/…, rather than exposing a public bucket listing. Small and medium previews can be viewed with live requests. Opening the full image requires the viewer’s own active request to contain a photo. The image endpoint uses Cache-Control: no-store and checks that the image remains live and has not been removed.
Removal and propagation
Any visitor can use a photo’s three-dot menu and red X to remove it for the stated rights, copyright or legal concern. The server accepts that action without requiring an account or an active request. It checks the request origin and confirmation value, records removal, detaches the image from requests and wingfam permissions, and attempts deletion of the original and both thumbnails. If file deletion fails, later cleanup retries while the live endpoint keeps the image unavailable.
The reporting page clears its visible copies immediately after success. Other pages learn about removal through snapshots or photo-status checks, normally around every 10 seconds. A saved photo association lets the uploader’s browser recognize and clear its local copy when it reconnects. Offline, suspended and outdated browsers cannot be remotely erased immediately, and older copies may lack that association. Screenshots and external downloads are beyond the app’s control.
Removal receipts retain the photo ID, an associated request ID when available, and removal time without a fixed expiry. They contain no image or reporter identity. The tool removes references to the selected upload, not every separately uploaded duplicate, and it does not decide whether a legal violation actually occurred. See the removal terms for the rationale and reporting options.
Content and access controls
Text and emoji screening
A deterministic phrase-and-symbol filter screens submitted names, plans, addresses, recognition details and relevant phone input for prohibited dating, marriage, sexual and drug-related content. It checks common slang, some disguised spellings, Unicode lookalikes and a defined set of emoji, including arrow symbols. Drug screening covers explicit drug names, common use and dealing phrases, some separated or disguised spellings, and associated emoji. Ambiguous everyday words require drug-related context, while the defined drug-associated emoji are blocked even when intended innocently. Numeric and fixed-choice fields have their own schema validation.
Checks run in the browser for feedback and on server submission paths, including request updates and wingfam reopening. Server validation matters because browser checks can be bypassed. No external AI moderation service receives the text for this filter. It does not interpret every innuendo, understand every language, inspect image meaning or establish intent. False positives and evasions are possible. The screening release history below records the banned emoji inventory introduced with screening.
Revision history and change links
- Introduced in v30.
- Drug screening added in v50.
- v30 changelog
Security and practical limits
HTTPS, restricted session cookies, hashed token lookup, parameterized database queries, input validation, origin checks, rate limits and conditional database updates provide layers of protection. They do not prove someone’s identity, make posted data private, prevent every misuse, or guarantee that multiple browsers belong to different people.
Names, locations and photos visible to other visitors can be copied. Browser storage can be cleared or edited. Map data can be incomplete, network responses can be delayed, and a successfully hidden record can still exist in backup history. The implementation deliberately favors temporary live availability and simple coordination over permanent accounts and guaranteed background delivery.
For the rules governing use, read the Terms of Service. For data handling and retention, read the Privacy Policy. For a problem or rights concern, use Report a concern.
Client experience and operations
Live connection and suitability diagnostics
Diagnostics explain what the page last observed and which policy it is configured to follow. Opening the masthead badge does not run an independent health check or inspect database cache rows. This distinction matters when a failed latest lookup coexists with a previously selected, still-retained meeting point.
The masthead diagnostics button keeps a 44-pixel minimum width and cannot shrink in the header. At viewport widths of 420 pixels or less it is a 44-by-44-pixel circle with the text hidden; larger views retain the expanded status label.
The connection icon uses a steady radar grid with a clockwise sweep from 12 o’clock back to 12 o’clock every ten seconds. A 135-degree gradient tail fades behind the beam. Six fixed decorative blips brighten as the beam passes and gradually fade; they do not represent people or measured activity. The animation is a local display cycle, does not trigger network checks, and continues smoothly across successful polls. Connecting and failed states pause the animation and keep their existing status colours. Retry attempts while disconnected keep it still until a successful response restores the connection. Reduced-motion preferences show a still radar.
Panel behavior and interpretation
The blurred panel opens at the right below the visible quick links bar, 330 pixels wide when space permits, and scrolls within the remaining viewport height. Drag its header with a mouse, pen or touch to move it; the Drag to move control also accepts arrow keys (10 pixels, or 40 with Shift). Movement stays within the visible viewport and below the quick links bar. Resizing, scrolling and mobile viewport changes recheck those bounds. Closing and reopening resets the position. Pointer movement is combined into one direct transform update per animation frame using cached bounds, without rerendering diagnostic content or measuring layout on each movement. Available height changes only when needed; the size observer watches the header and quick links rather than the moving panel. The header and close button remain visible while the diagnostic body scrolls. Its stationary background layer blurs the page behind it while the content scrolls separately; a tinted surface keeps the text readable, with an opaque fallback when backdrop filtering is unsupported. The static theme stylesheet preserves both standard and WebKit blur declarations through publication. Its close button and Escape return focus to the badge. Sections expand independently. Live counts and Connection start expanded; Sound & vibration, Your session, Meeting-place suitability, Browser details & timing notes, and Log start collapsed on each opening.
| Value | What it represents | What it does not establish |
|---|---|---|
| Connection / lease / snapshot age | Last successful API response, attempt timing and server-derived lease countdown. | An uninterrupted connection or one-second network polling. |
| Selected-point check | Last client selection/validation attempt, reason and age; the current pin is listed separately. | That a failed latest replacement removed an older valid selection. |
| Current-location suitability | Separate device screening result, reuse deadline and original source/data time when supplied. | Approval of the selected meetup point. |
| Cache policy | Configured lifetimes and coordinate tolerances. | A cache hit for a particular lookup. |
| Result source | Observed exact-point memory/D1 hit, reused geometry, fresh provider or shared in-flight work. | An independently verified provider-health result. |
Round-trip duration includes matching-server processing and JSON handling. The lease countdown combines server time with elapsed time since receipt. Counts become labeled stale as their observation ages. A local one-second display timer runs only while the panel is open; opening or expanding sections adds no provider calls, telemetry or persistent diagnostic record. Location, radius and map-bound changes normally ride the next ten-second state poll.
Location and cache provenance
The session section separates the active approximate-IP-area flag from the last IP attempt. The latter records fresh-provider versus sessionStorage provenance, original result time, reuse deadline and whether saving succeeded. Expiry prevents later reuse but does not erase an already displayed fallback or trigger another lookup. A late IP result that loses to device acquisition is reported as not applied.
City diagnostics distinguish location basis from resolution method: Worker memory, shared D1 cache, fresh lookup, in-flight sharing or hosting IP geography. Negative cached results identify the server layer and up-to-30-second cooldown. There is no browser city-resolution cache; the separate tab sessionStorage cache holds an approximate IP browsing area for one hour.
Suitability diagnostics distinguish exact-point server reuse (up to 24 hours), geometry reuse (up to 24 hours within 5 metres, with a new classification), and current-location page-memory reuse (up to 24 hours within 5 metres, with a 30-second failed-check cooldown). Derived classifications inherit geometry expiry. An exact-point result may no longer expose its earlier geometry provenance, so no separate geometry-source field is claimed. Shared in-flight timestamps can be unavailable after persistence failure.
Restored draft/history coordinates never restore an approval. Use my location can select an already prepared business. A paused device check sends no request, and suitability is not persisted in localStorage or the HTTP browser cache. The selected-location details show distance from the current location rather than distance to themselves; diagnostic units spell out meters.
Bounded provider and coordination log
The page log retains failed provider attempts even when a later fallback succeeds. Each entry has a UTC timestamp, one-word purpose and hostname, followed by HTTP status and provider output, or a network-failure explanation. HTTP 200 timeout remarks and malformed JSON count as provider failures. Map-tile and script loads are outside this fetch-based log.
- Provider output is capped at 4,096 characters per attempt, with at most 20 attempts per response and the latest 100 entries in page memory.
- Lookup coordination entries record cache hits, starts, waits, expired-lease recovery, deadlines and storage fallback. They omit coordinates, query text and owner tokens; provider failures take priority within the response limit.
- Query strings are excluded from provider labels, although provider output can echo searched locations. IP-fallback event entries contain no IP address or coordinates.
- Concurrent shared work is attributed to the initiating request; cancelled losing fallback attempts are omitted. Lost or cancelled responses can prevent their logs reaching the page.
- The log survives closing the panel and clears on reload. It creates no shared visitor log or persistent diagnostic database.
The read-only log starts at 180 pixels high and can resize between 120 and 360 pixels within the scrolling panel. Entries are adjacent without extra blank lines; line breaks within provider messages remain. These presentation controls do not change the collection limits.
Revision history and change links
- Ten-second radar sweep and fading decorative signals added in v96.
- Interactive diagnostics introduced in v44.
- Separate blur layer and collapsed sound controls added in v88.
- Published Chrome blur declaration restored in v89.
- Draggable diagnostics and quick links clearance added in v90.
- Dragging rendering and resize feedback optimized in v93.
- Circular compact badge sizing with an expanded label on larger screens corrected in v87.
- Suitability and device-reading information added in v52.
- Collapsible sections added in v53.
- Live counts opens expanded by default from v60.
- Location diagnostics clarified in v70.
- Separate suitability cache policies added in v66.
- City cache labels clarified in v63.
- The static Browser city cache and Persistent browser cache rows were removed in v64.
- Suitability provenance added in v61.
- One-second local display updates restored in v65.
- Provider failure log added in v56.
- Log scrolling and spacing improved in v68.
- Lookup coordination events added in v58.
Appearance preferences
Appearance is a browser preference shared by the homepage, document pages and standalone changelog. A header control cycles Auto → Light → Dark → Auto; Auto follows the device color-scheme setting and is the default.
An appearance controller embedded in the initial document applies the saved preference before a separate script download is needed. It reapplies the mode when the interactive page is ready, follows device changes, and synchronizes ordinary tabs through storage events. Repeated initialization reuses the controller and current preference. Dark mode retains blue/yellow accents on charcoal/slate surfaces.
The preference is stored under wingman-theme and is not sent to the server. If storage is unavailable, switching still works for the current page but does not survive reload.
Active request form
While the server snapshot contains an active request, the connection is healthy and no wingmatch is underway, the request panel shows a moving gold dashed border. The border is decorative and does not intercept pointer input. It is static when the browser requests reduced motion and disappears when those update-mode conditions end. The request panel’s lightning icons are filled yellow only while its update-mode marquee is active; new-request and other lightning icons retain their outline appearance.
Searchable visitor FAQ
The /faq page contains 25 questions from one shared content collection, including device installation guides, sponsorship, matching, request continuity and privacy. Search checks question text, answers, linked resource labels and keywords without sending the query to the server. Topic and search filters combine. All answers start collapsed, with a single keyboard-accessible question-and-arrow button per answer; multiple answers may stay open. Filtering keeps expansion state in page memory, and Expand all and Collapse all affect only visible results. No-results recovery clears both filters.
Stable question fragments open and focus the requested answer after clearing search and topic filters. A separate Copy link button copies only the page address and question fragment, removing any query string; success is announced after the Clipboard API resolves, and failure gives retry feedback. Search and expansion state are not stored across reloads. All question and answer text is rendered in the initial HTML, including collapsed answers. The page uses the shared document shell, themes, back-to-top and refresh behavior. Footer links open a new browser tab or the existing installed-app overlay. The homepage installation instructions link directly to /faq#install. Device instruction links open externally and remain subject to browser and operating-system behavior.
Revision history and change links
- Introduced in v78.
In-page alerts, sound and haptics
Notifications report events observed by the open page. Toasts and relevant match transitions can trigger sounds and haptics, but the application has no native notification-permission flow, push subscription or background browser-notification delivery.
Playback and independent controls
Sound uses small first-party PCM files and one reusable HTML audio element. The first interaction primes it with silence. Test sound invokes playback directly from the tap; rejection is handled and diagnostics report failure. The speaker animation follows media events, while prioritization and brief coalescing reduce overlapping bursts.
| Channel | Preference and test | Platform boundary |
|---|---|---|
| Sound | Defaults on; speaker switch beside theme control. Independent diagnostic switch/test and feedback. | Browser interaction may be required. A playing element does not prove physical audibility. |
| Haptics | Defaults on; independent switch/test. Disabling cancels queued/running vibration. | Uses supported Vibration API after interaction while visible; unsupported channel shows Unavailable. |
Both preferences persist locally and synchronize across same-site tabs. Tests are disabled when their channel is off or unsupported. The speaker help popover is available by hover or its question-mark control, and diagnostics group both channels under Sound & vibration.
Where supported, enabled sound requests the Audio Session API’s media-playback category and restores the earlier category when disabled or removed. Media volume and platform behavior still apply. Installing the app does not remove those restrictions or enable guaranteed background alerts.
Installation and offline behavior
The site can be added to a device’s home screen on supporting browsers. The browser title is “Wingman! - The original free service”; the existing installation names remain “Find a Wingman!” and “Wingman!”. Its service worker caches a small set of installation assets and an offline page. Navigation attempts the network first and can fall back to that offline page.
The service worker does not cache live request responses or contact details and does not run a background matching engine. Installing the app does not remove browser suspension, connectivity or permission limits. Matching and posting require the server; saved drafts are convenience data, not an offline queue of requests waiting to go live.
Versions and updates
Startup readiness and release discovery are separate mechanisms. The loading screen protects the initial transition into a usable page; the later update banner lets an existing visitor decide when to reload a newer release.
Document startup
The initial HTML activates the loading screen before showing the app. Homepage and shared documents wait for the document load event, observed scripts/styles, fonts and interactive-page readiness. The standalone changelog does not require application hydration. A resource failure or a 20-second delay offers Try again, which performs an ordinary reload; a slow successful startup can still finish automatically.
Readiness is retained for that document, including repeated or late initialization. Once ready, loading listeners and resource observers are removed. Theme changes, later resource failures, ordinary updates and returning to the same retained document do not restart the loader. A new document or explicit reload starts a new cycle, and browser suspension can delay it.
Release discovery and asset freshness
The version badge opens the generated changelog in a new tab. While visible, the page normally checks for a release every 30 seconds and on relevant return/connection events. A newer release offers a refresh and changelog link; the app does not force a reload during a request.
| Item | Freshness contract |
|---|---|
| First-party static assets | Theme styles/scripts, document-refresh code, sounds and icons use hashes of their contents. Only a changed file gets a changed URL. |
| Installation manifest | References hashed icons and has its own content hash. |
| Framework JavaScript / CSS | Use build-generated hashed filenames. |
| Refresh action | Ordinary reload obtains current asset references while preserving cookies, local storage, saved requests and preferences. |
The homepage, document shell and standalone changelog use the same generated asset map. Releases build the application and include database migrations before publication. Significant design/workflow changes require a full review of this document; smaller changes update the relevant passages, diagrams and revision links in place.
Revision history and change links
- Version badge and changelog introduced in v12.
- Optional refresh banner added in v23.
- Banner changelog link added in v30.
- Loading screen introduced in v79.
- Document-lifetime loading and delayed startup recovery fixed in v83.
- Version-badge navigation updated in v76.
- Content-based static-asset URLs added in v49.
How it’s Built
Vibe coding as an ongoing conversation
Wingman was developed through a conversation between Todd Hendricks and an AI coding agent, using the Codex desktop app and ChatGPT Sites tools. Todd describes the intended experience, tries the result, and steers the next change. The agent reads and edits the application source, investigates behavior, runs checks and publishes versions. The visible footer credits “ChatGPT 6 Astra/Medium”; that credit identifies the project’s stated development attribution, rather than documenting the model settings of every individual tool call or past session.
Here, “vibe coding” means expressing much of the product direction in ordinary language while the agent handles implementation. The initial brief was substantial: a login-free way to find company, a radius slider and map, temporary requests, reciprocal matching, agreement on a meeting place, recognition details and optional mutual phone exchange. It also specified a superhero-inspired identity. The midnight-blue and yellow interface and bold Wingman! wordmark grew from that brief.
The result is maintained as source code with a database schema and release history. Conversational development still requires decisions about race conditions, browser restrictions, validation and retention. A screen that looks plausible is only one part of the result: the shared state must behave correctly when two people click, disconnect, return or change their minds.
The tools and their responsibilities
ChatGPT Sites and the Codex desktop workspace
ChatGPT Sites provides the site project and publication workflow. The agent works with a local source checkout, then uses Sites tools to save a version associated with a pushed Git commit and deploy that saved version. The published application runs on the hosting infrastructure described in the architecture section: Cloudflare Workers, D1 and R2. Development conversations and publication tools are distinct from the request-matching code visitors use.
Codex desktop provides the development conversation, access to the working files and terminal, and an in-app browser for inspecting the site. Todd can comment on an exact element in the preview instead of describing its position from memory. That feedback can arrive during ongoing work: a wording correction, a new constraint or an additional example becomes part of the current change before it is released.
MCP and tool integrations actually used
MCP—Model Context Protocol—provides a common way for an AI application to expose tools and context from other systems. In Wingman’s development records, tool calls cover several distinct jobs. These integrations assist development; visitors do not need to install these servers to use Wingman.
- Sites tools through the codex_apps interface
- Create and inspect the site project, obtain short-lived source-repository credentials, save versions, deploy them and check deployment status. Later changes reuse the existing project and preserve its audience.
- codebase-memory-mcp
- Index the source into a knowledge graph and locate relevant code by function, route or relationship. Workspace guidance prefers this graph for code discovery, with targeted text searches when the graph is insufficient or the task concerns literal text or configuration.
- Computer-use/browser tools
- The cua_repl browser interface has been used to inspect rendered pages and operate controls. Browser comments supply the selected element and visual context. Separate Playwright scripts also exercise isolated browser scenarios; those scripts are test tooling, rather than another production matching service.
- Codex app tools
- Open the relevant preview and read earlier development tasks when established decisions or history are needed. This section was checked against Wingman’s earlier creation and refinement conversations as well as the current source and changelog.
Ordinary development tools also matter: Git records source changes; Node.js runs the application toolchain and JavaScript tests; TypeScript checks types; SQL migrations evolve the database; and browser automation checks actual controls and layouts. Tool availability does not establish that a particular tool was used. This account names integrations evidenced in Wingman’s development, rather than listing every server available in the author’s environment.
How human steering changes the implementation
Feedback ranges from visual detail to product semantics. A comment attached to the nearby-visitor count can specify the spacing above and below it. A follow-up can reorder three footer lines and then ask that they read as one compact block. These are precise refinements of the rendered experience, and the agent checks the resulting layout at phone and desktop widths.
Other instructions alter shared behavior. Previous requests became duplicates when all saved fields except photo and meeting time matched. Gender preferences gained an explicit explanation about comfort and personal safety when choosing platonic company. Recognition choices introduced a new conflict: two people both choosing “I’ll find you.” Resolving that request required a stored recognition mode, a message visible to both participants, and server-side refusal to finalize until one chooses another option.
Steering can also correct an earlier rule. The technical document exposed an obsolete “tomorrow before 6 AM” restriction. Inspection showed that it still existed in both the browser and server, so the implementation was changed to remove the cutoff and the rolling 12-hour window was tested. The document was corrected alongside the implementation. Conversation history helps explain why a feature exists, but current code and verified behavior determine what the site can honestly claim.
Diagram
Text version
- Describe a goal or annotate an element in the preview
- Inspect the relevant source and translate feedback into behavior and acceptance checks
- Edit the UI, server rules, storage and documentation as needed
- Test normal behavior, failure cases and relevant screen sizes
- Publish a version, inspect the live result, and use the next observation to steer again
Revision history and change links
Project instructions that survive the conversation
An AGENTS.md file keeps recurring project expectations beside the source so a later coding session can find them. It is guidance for development agents, not executable enforcement. Its value comes from making obligations explicit and then checking that the resulting changes follow them.
Release traceability
Every release needs clear, user-facing changelog entries. Versions stay in descending order with dated records, related edits in one unpublished batch share a release, and the generated changelog pages must agree with the source history. Agents must not invent historical fixes or publication claims.
Choosing the review scope
A significant design or workflow change requires a review of the entire How it works page against the final implementation, including sections that were not directly edited. This covers changes to user flows, state transitions, architectural responsibilities, synchronization, recovery, data lifecycles and privacy boundaries. The review follows their effects through prose, diagrams, tables, caches, storage, diagnostics and limitations. Several small changes that collectively alter a flow also require this full review.
Smaller wording, styling, label, threshold and isolated bug-fix changes keep the existing update-in-place workflow. The agent updates affected passages and related diagrams, tables and revision links in their established sections, then checks nearby explanations and cross-references for contradictions. A focused review expands to a full-page review when it reveals wider effects or accumulated inconsistencies. Superseded statements are corrected or removed instead of being followed by contradictory additions.
Consistent revision history
Every section with introduction or change-version links ends with one collapsed block labeled “Revision history and change links,” with the links presented as list items. That block follows the section’s prose, diagrams, tables and any child sections. Each subsection owns its own references; a parent’s separate history belongs at the bottom of the parent. Sections without revision links need no empty block.
Change-version links do not appear before the body, inline in explanatory paragraphs or in a second revision block. Moving a link must leave complete sentences and preserve its descriptive label and historical destination. Before release, the agent checks the entire rendered explainer for links outside these blocks, duplicate or misplaced blocks, expanded defaults and broken changelog anchors. The implementation-review label and ordinary navigation have separate purposes and are not feature-history entries.
Keeping the instructions and this account synchronized
Every modification to AGENTS.md must also be documented in the appropriate How it works section during the same unpublished release. This applies to additions, revisions, removals, reorganizations and editorial clarifications, including changes that do not affect the running application. Guidance about agent work, review scope, documentation conventions and release obligations is maintained here; other affected sections are updated when the instruction also changes a documented workflow or product behavior.
The explanation describes the resulting rule and its practical effect. Editorial-only changes are identified as clarifications rather than new obligations or runtime features. The agent records the change in the changelog, places its revision link in the owning section’s collapsed history, and checks the final instruction diff against the corresponding passages before publishing. Both focused updates and full-page reviews require this synchronization; an instructions-only commit does not complete a release.
Safety documentation and review cadence
At the start of each change, the agent checks whether a safeguard is being added, changed or removed. The safety page is updated in that same release for affected privacy and sharing controls, consent, matching, location and content screening, photos, activity signals, expiry, recovery or reporting. New safeguards are documented immediately rather than waiting for a periodic review.
A full safety-page review becomes due when ten versions have passed since its last full review and must be completed before publishing a release that reaches a fifteen-version gap. The agent compares both the current verified release and the intended release with the safety page’s explicit full-review version. The How it works review label and the page’s last edit do not establish that baseline. A missing, invalid or future baseline must be resolved or replaced by a completed full review, and concurrent publication requires checking the gap again against the final release number.
The full review reads every safety section against the final source, including safeguards untouched by the current change. It checks public and private boundaries, normal operation, failures and recovery, limitations, reporting routes and relevant Privacy and Terms references, supported by appropriate tests and rendered inspection. A changelog or earlier review label is not sufficient evidence, and gaps are corrected without inventing protections or guarantees.
Every completed safety review records its actual America/Chicago date, examined release and scope/outcome, even when no prose needs changing. The full-review label uses “Full safety review for Wingman vN · Month Day, Year.” A focused review can have a separate dated and versioned label, but preserves the last full-review baseline so it does not restart the ten-to-fifteen-version clock. Builds, page views and unrelated releases never advance these labels automatically; incomplete reviews are not marked complete. Safety review dates remain separate from policy modification dates, and the agent verifies the rendered label, links, section navigation and release references before completing publication.
Policy accuracy, navigation and verification
Changes to collection, sharing, retention or user controls require review of the Privacy Policy and affected Terms. Policy modification dates reflect the actual date that policy was edited; a routine build or unrelated feature does not advance them. Footer and policy links open in a new tab, and section anchors and the sitemap remain usable.
For either review scope, the agent checks normal operation and failure/recovery paths, including delayed results, restored state, permissions, provider failures, retained values and retries. Location descriptions distinguish device readings, IP fallback, the viewport, the selected pin, business selection and suitability. Cache descriptions identify layers, keys, positive and negative lifetimes, finite limits, stale reuse and physical cleanup. Diagnostic descriptions separate configured policy from observed results and explain defaults and retry triggers.
The implementation-review label names the release actually reviewed. Appropriate checks and rendered inspection accompany publication, with material verification limits reported honestly. Simulated tests do not establish physical-device or live-provider behavior, and documentation must not promise identity verification, delivery, moderation or deletion that the implementation cannot provide.
Revision history and change links
Examples from the earlier build
Live status needed database evidence
Early feedback questioned whether a request was truly live. An early correction required database confirmation before displaying that status, and a subsequent change added live-request recovery. This is a recurring design lesson: a local interface state is not proof that a shared server operation succeeded.
A business dropdown needed a failure path
Nearby businesses began as a convenience feature in an earlier release. A later report reproduced a failing lookup on the published site. The fix improved the query, provider fallback and caching, and made the unavailable state recover through repeated checks. The creation chats distinguish a broken local preview from a confirmed production failure; one is not sufficient evidence of the other.
Photo limits became a processing pipeline
The photo feature evolved from its initial upload control into a tile interface, browser preparation, server validation and compression, and multiple thumbnail sizes. The input allowance eventually reached 3.5 MB while keeping a smaller processed-image limit. Asking for a larger upload was therefore a change to both usability and resource handling.
Small words can encode different events
A request disappearing after deliberate withdrawal is different from a request expiring because its heartbeat stopped. A later correction removed the misleading expiry notification for intentional withdrawal. That change is a useful example of why testing the reason for a state transition matters, not just the final empty screen.
From an edited preview to a published release
The release workflow builds the application, checks types and runs tests appropriate to the change. For behavior involving shared state, tests can run actual SQL migrations against an isolated SQLite database and exercise the API. Browser tests can supply controlled snapshots to check dialogs, permission-dependent behavior and narrow layouts without posting artificial meetup requests to the live service. Provider checks and production readbacks cover different questions from those controlled tests.
The source is committed and pushed before a Sites version is saved. The deployable archive contains built output and required migration/configuration files, and is associated with that exact commit. A saved version is then deployed; the same deployment is checked until it succeeds or fails. After success, the live version endpoint and relevant pages or flows are inspected. Existing visitors receive the optional refresh banner rather than a forced reload during a meetup flow.
Passing a test demonstrates the scenario it exercised. A mobile-sized desktop browser test does not prove every behavior on a physical iPhone or Android device, and a mocked provider response does not establish that the provider is healthy. The process therefore combines source inspection, targeted tests, rendered checks and live verification, while documenting the limits of each.
Where’s the source?
Wingman’s source code is not publicly available yet. Development uses a private source repository, as described in the release workflow above. The ability to work with that repository does not itself make the project public or establish a general user-facing Sites export feature. There is no announced public source release date or public source-code license.
This document and the changelog provide a view of the design and how it evolves in the meantime. You can follow those pages for updates about the project and source availability.
Revision history and change links
- Added in v37.
What the AI does—and what the running site does
The AI is used to build and maintain the software. The matching decisions, expiry rules, content screening and location classification described above run as application code and database operations. The current matching flow does not ask a language model to choose a companion, interpret a person’s intentions or approve their safety.
Todd supplies the product direction and remains the site operator. The agent can implement and investigate changes, but generated code, tests and policy prose can still be wrong. This document and the changelog make the design and its evolution inspectable; they are not a substitute for operational experience, independent review or appropriate professional advice when needed.
Revision history and change links
- This development-process account was added in v35.