System architecture and boundaries
The architecture
Core service launched in v1.
Wingman is a browser-based service for arranging platonic, real-world meetups. A request describes a person’s plan, meeting place, preferred time and travel radius. It is a temporary availability signal, rather than a permanent account or inbox.
Browser and server responsibilities
The browser: a React and TypeScript interface renders forms, the radar map, requests and match dialogs. Local state makes editing responsive; browser storage preserves selected convenience and recovery data.
The application server: API routes deployed on Cloudflare Workers validate inputs, identify the current session, renew availability, enforce matching rules and return a fresh view of the database.
Persistence boundaries
The database: Cloudflare D1, a SQLite-based database, holds live requests, matches, participant membership, rate-limit records, photo metadata, wingfam reopening permissions, photo-removal records, visitor geography caches, meetup classifications, address/business-name search results, reusable map geometry, raw business-query results and short-lived lookup leases.
Photo storage: Cloudflare R2 holds uploaded images and thumbnail files. Images are served through an application endpoint that checks availability and access.
The project uses Vinext with Vite and a Next.js-style route structure. UI components do not write directly to the database or storage bucket. Server routes are the authority for shared state.
Identity, presence and synchronization
How it works without a login
Login-free matching launched in v1. Live request recovery added in v7.
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
The current 10-second polling cadence was introduced in v9.
The browser normally posts a state action to /api/wingman about every 10 seconds. A successful renewal extends the request’s server-side expires_at by 90 seconds. The response is a snapshot containing the browser’s own request, visible and nearby requests, map dots, counts, and its current match when one exists.
Posting, accepting, changing a meeting place, saving recognition details and finalizing also use this API. A successful action returns an updated snapshot immediately. Other visitors usually see the change on their next successful poll. This is HTTP polling, not a WebSocket connection or server push stream.
Request age added in v70. Radar cards, map quick info, request details, your live request and wingmatch views show elapsed time from the original request creation timestamp, in minutes, hours or days. Saved history uses its recorded original creation time; reusing it starts a new age when the new request is posted. Editing or renewing the same request does not reset its age. Displays use the latest server clock observation when available and refresh locally every second while timers run, without extra network requests. Request age does not indicate remaining expiry time or last activity. Beside it, Last check-in is derived from the server-provided lease expiry minus its 90-second lifetime; a state heartbeat or another authenticated request action can renew that lease. It shows elapsed seconds, marks check-ins delayed after 30 seconds and warns of possible expiry after 60 seconds with a conditional remaining-seconds countdown. At the observed deadline it says Expiry due — awaiting server update; a newer snapshot can show a renewal. This is the last server observation, not proof of attention or a guarantee of expiry. A third age, Last browser activity, reports the latest browser-observed interaction with this visible Wingman page: pointer movement or presses, wheel/touch input, typing, and scrolling near trusted input. The observer keeps only one page-memory timestamp, without event contents, coordinates or history. It does not treat automatic scrolling by itself, synthetic events, hidden-page events or visibility changes as interaction. Existing request calls carry elapsed milliseconds only for a live request or a create/wingfam action; no event-triggered network call is added. The server stores the newest inferred timestamp on that request, ignoring older same-request reports from other tabs. Incoming elapsed values are bounded to seven days; missing observations keep the stored value unchanged. Network delay and browser clocks make the age approximate and it is not proof of a person’s attention. It neither extends the lease nor changes matching. Old clients and requests without an observation show Not observed. Saved history does not retain activity metadata. The nullable database field is deleted with its request under the existing cleanup rules; previously received snapshots and the temporary final-match recovery copy can retain it. Open details receive updated lease and activity times from new snapshots. Quick-info text updates without rebuilding map markers just for clock ticks. Missing timestamps show Unavailable; saved history has no ongoing heartbeat and says Not tracked in saved history.
Concurrency and connection failure
Within a page, guards prevent overlapping routine polls and avoid applying a poll while a mutation is in progress. Matching calls have a 12-second timeout; failed requests show a connection problem and later checks retry. The interface also marks the connection unavailable when the last success becomes too old. Reconnection and page-return events trigger refreshes.
Polling is a target cadence, not a delivery guarantee. Network delays, mobile power management, browser throttling and suspended tabs can delay it. A background page may continue sending heartbeats, so hiding a tab does not necessarily end a request. A closed or suspended page cannot be relied on to renew it.
Multiple tabs in one browser
Introduced in v31.
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.
Worldwide visitor presence
Search-center fallback corrected in v38.
Without an open request, nearby discovery and counts use the selected meetup point, then detected device location, then the approximate IP area as the search center. Detecting a location refreshes this view even when there is no eligible default meetup point. A visitor at home can therefore count themselves and browse nearby activity without posting a request. Using a location as a browsing center does not approve it as a meetup point; residential screening still applies before posting.
Worldwide request counts added in v4. Worldwide and nearby visitor counts added in v32.
Two counts with different meanings
The worldwide request count is the number of live request rows. The viewer count is the number of unexpired browser-presence records, including browsers that have an open request. These populations overlap and must not be added together as a total of people.
Short-lived deduplication
A visible homepage supplies a random UUID with its regular API calls. The UUID and its 90-second reuse deadline are shared in local storage under wingman-viewer; they are separate from the session capability that controls a request. The server stores a SHA-256 identifier hash, expiry, available location and temporary request-session hash in D1. An upsert renews one row, so coordinated tabs normally count once. Hidden pages omit visitor renewal even if request heartbeats continue.
The count includes visitors seen during the last 90 seconds, rather than measuring exact screen attention. Expired rows do not count and are deleted on later API activity. A later visible visit replaces an expired local identifier; inactive browser storage is not physically erased by a timer. If storage is unavailable, a page-memory fallback keeps that tab stable but cannot deduplicate other tabs.
Nearby non-requesting visitors
The radius panel counts unexpired visitor records inside the current search area that have no live request associated with their session hash. The client supplies detected device coordinates, falling back to the existing approximate IP-location result. A visitor without usable coordinates still counts worldwide, but cannot count locally. An eligible viewing browser includes itself.
The server uses the same great-circle distance calculation and selected search center as the request search. It returns only the aggregate count, never a public list of visitor identifiers or coordinates. Visitor location and request-session association expire with presence; cleanup is opportunistic. A request completion or withdrawal changes the exclusion on the next snapshot. Coordinates and browser identifiers are client-supplied, so this is not a proof of physical presence.
City breakdown and contextual labels
Worldwide city counts added in v43. Clicking the worldwide viewer number opens a live city breakdown. Each visible presence renewal stores city, region and country labels on the existing visitor row. Device coordinates take priority: the server rounds them to three decimal places and reverse-geocodes them through the existing Photon provider. Persistent city-lookup caching added in v44. Coordinates share a 0.001-degree grid cell, about 100 metres at the equator. The database stores successful labels for 24-hour reuse and negative results for 30 seconds, independent of visitor/session identifiers; shared background cleanup removes expired rows. A bounded 500-entry memory layer reuses results for up to five minutes, and concurrent lookups share in-flight work. An uncached lookup has a shared six-second time limit. Broader city selection added in v45. The first reverse response is checked for genuine city, town or village fields or a city-layer feature name; locality, district and subdivision names are not promoted to cities. If no such label is available, a second Photon reverse query requests up to five city-layer features within 20 km. The first result matching the known country and state/province is used. This is a nearby municipality/community label, not a claim of legal municipal membership or postal-city identity. Unsupported or failed results fall back to IP geography or remain unavailable. The v45 migration cleared earlier lookup rows and visitor labels; city-rule-versioned cache keys prevent reuse of the previous interpretation. Visitor labels rebuild on the next heartbeat. When device coordinates or usable city results are unavailable, the server uses the hosting request’s Cloudflare geolocation metadata. If hosting city labels are absent but approximate IP coordinates are available, those coordinates can be reverse-geocoded as an IP-based fallback. The server ignores geographic labels submitted in the request body. Device coordinates remain client-supplied rather than verified physical presence. Both reverse-geocoded and fallback labels are approximate; IP fallback may be inaccurate on mobile networks or VPNs. Missing cities are counted as unavailable.
The server groups unexpired visitor rows by city, region and country, orders by count descending with alphabetical tie-breaking, and returns at most 50 groups plus totals for other and unknown cities. These aggregates include viewers who have requests. Ordinary tabs still share one identifier, and geographic labels expire with the same presence record. The response also supplies the viewing browser’s approximate region and country. The popover omits a matching country and omits a matching region within that country, while preserving the complete group identity. Unknown viewer geography causes available labels to remain visible. The displayed list adapts to popover height up to eight rows; smaller screens show fewer cities and an Other cities total. Count and distance sorting added in v46. Radio buttons select Most viewers (highest count first) or Nearest cities (shortest distance first, unavailable distances last). The server ranks all active city groups before returning up to 50 entries for each order; each order has its own omitted-viewer total. The selected radio stays in page memory while the popover is closed. Distance is the straight-line distance from the browser’s supplied device or IP-fallback coordinates to a public representative city point, never to another visitor’s position or the chosen meetup point. The existing Photon provider is queried with city name, state/province and country, and only matching city-layer point results are accepted. Public city-point cache rows contain a geographic name key, coordinates and expiry, with 24-hour successful reuse and 30-second negative reuse; expired rows are deleted by shared background cleanup. The city-point request has a three-second timeout and concurrent same-city calls share work. No visitor identifiers are stored in this cache. The API returns distance values, not visitor coordinates. The interface uses browser-locale measurement conventions and localized number formatting, including an explicit locale measurement-system extension when supplied. Missing origins disable distance sorting; missing city points show Distance unavailable. These are approximate distances, not routes or travel estimates. No visitor identifiers or coordinates are exposed by this breakdown.
Accuracy and trust limits
This is an estimate of active browsers, not identity verification or historical analytics. Separate devices and profiles count separately; shared browsers can represent several people. Simultaneous first visits can briefly race before sharing an identifier, and automated clients can inflate the count. The service does not fingerprint visitors or deduplicate by IP address.
Discovery and meetup agreement
Maps, location and business search
Leaflet displays OpenStreetMap tiles. The browser can request device location; if that is unavailable, an IP-location lookup can supply an explicitly approximate area. Users can search a destination or choose a map pin. The selected meetup address and coordinates become public when a request is posted; they should describe an appropriate public meeting place.
Provider queries and filtering
Address search and pins launched in v1. Nearby businesses added in v15. Radius-aware business distances added in v20.
Address search and reverse lookup go through the server to Photon. Nearby businesses come from Overpass queries against OpenStreetMap data. Queries cover a bounding box, then the app computes distance, removes duplicates and private-access results, applies the chosen radius, sorts nearest first and returns up to 30 businesses. The business-distance choices are constrained by the user’s search radius.
Adult-venue screening added in v31.
Adult-entertainment venues are excluded when category tags or names identify them as adult shops, strip clubs, brothels or related businesses. The filter applies to business and address results, and obvious adult-venue names are also rejected in submitted addresses. It is not a real-world inspection: incomplete tags and neutral names can prevent a place from being recognized. Coordinate screening adds another check but does not establish the true identity of every venue.
Map overlay rendering
Map labels simplified in v67. The selected meeting address and distance details appear immediately above the main map. Its legend identifies meeting pins, live requests and the device marker when available. Search-radius shading remains on the map, but has no separate legend item; the radius control specifies the search distance.
Close-zoom rendering improved in v42. Search-radius and approximate-area shading use 360-point geodesic polygons that Leaflet clips to the viewport. This avoids rendering enormous offscreen dashed SVG circles at street-level zoom. These display approximations do not change the distance calculations that determine search eligibility. Request markers remain separate, interactive and keyboard accessible. The main map saves its displayed center and zoom in browser local storage and restores them on reload; compact match maps keep their own behavior. Stored values are validated and storage failures do not block the map. Precise-location initialization revised in v53. Independent device centering added in v54. When no meetup pin is selected, the first precise device view overrides a saved wide IP viewport and centers on the device fix, and applies the appropriate zoom, including when the fix arrives before Leaflet finishes loading. The selected meetup coordinates are not changed by recentering. Phone framing and acquisition notices refined in v55. Before a meetup pin is selected, the current device point is screened separately for map presentation. Device-marker visibility and selection zoom updated in v62. Every zoom level shows a blue dot at the latest device coordinates and a translucent halo for the reported accuracy estimate, when available, including beside a selected meetup pin and while suitability is unknown or unsuitable. The marker has a fixed screen size so it remains visible when zoomed out, provided its coordinates are inside the viewport. IP-only positioning continues to use the approximate-area overlay rather than a precise device dot. Its tooltip identifies retained readings as the last obtained location. The dot updates even when movement is too small to recenter the viewport. The close view automatically widens enough to contain the reported accuracy halo with padding, then tightens toward zoom 17 as accuracy improves. Accuracy-only changes adjust zoom without deliberately recentering; movement beyond 5 metres still controls following the device. This framing adjustment does not change the last confirmed suitability decision. An allowed result opens up to street-level zoom (17), centered on the latest device coordinates, so nearby business labels can be read where the map provider supplies them. Until a suitable result is known, the map uses the selected-radius view so nearby requests remain visible. After a suitable result, pending, unknown or failed checks preserve the close view; only a confirmed unsuitable result switches automatic browsing back to the radius view. This remembered zoom decision is separate from business preparation, which still requires a current allowed result. This also applies to retained device coordinates. The presentation check does not select a business or approve a meetup pin. Its most recent completed result is reused in page memory for up to 24 hours within 5 metres to reduce GPS jitter; this tolerance affects zoom and the business-preparation gate, not approval of a new meetup point. The latest location is sampled initially and about every 30 seconds while visible; movement beyond 5 metres or expiry triggers another check at that sampling step. Failed checks retry on a later device update after at least 30 seconds. In-flight obsolete checks are cancelled, and an active match or locked wingfam suspends these checks. Each actual meetup point is validated independently. Selecting an address, nearby business, prepared current business, previous request or another request’s meeting location centers the map on that pin at business-level zoom 17. Compact meeting maps also use zoom 17. A successful double-click or double-tap pin drop preserves the current map center, zoom and page position; normal eligibility checks still apply, and a failed check preserves the prior pin. Before posting, later device readings do not refocus a selected meetup pin. During an active request, when device coordinates are available, the main map instead fits just the meetup point and latest device point with 30 pixels of edge padding, up to zoom 17. It frames immediately on activation or the first available device fix, then coalesces coordinate changes into automatic updates at most once every ten seconds, using the latest points when the timer runs. The view tightens as the distance closes and widens as it increases; the search-radius circle does not determine this framing. Unchanged coordinates do not trigger another fit, and manual pan and zoom remain available between updates. Retained device coordinates are used with the last-location warning if a fresh reading fails. With no device fix, the meeting-pin view remains available; an approximate IP estimate is not used as the device endpoint. Withdrawing or losing the active request cancels pending automatic framing. A saved viewport is preserved on reload unless an explicit selection or other focus change occurs. Automatic fitting uses fractional zoom steps to reduce excess space around the selected radius on narrow screens. The device-acquired notice appears for five seconds when the loaded map first has a fresh device location or recovers from a location failure; periodic successful readings do not restart it. Selecting a meetup or receiving the retained-location warning hides it immediately. A visible IP-to-device transition also refocuses the map. In the radius view, routine device updates preserve the viewport. In the close business view, continuous device readings recenter the map only after movement exceeds 5 metres from the last automatic center, including while a suitability check is pending; a suitability-mode change recenters using the latest device coordinates; explicit meeting-point selection and search-radius changes can refocus it. The 30-mile approximate-area overlay and IP caption are shown only while IP positioning is actually in use. This saved viewport is separate from the device location and meetup point.
Device location, failure recovery and a fixed meetup pin
Periodic device refresh introduced in v41. Continuous walking updates added in v55. The browser requests device coordinates at startup and watches for new readings while visible. The watch stops when hidden and restarts on return. The browser and operating system choose the delivery frequency; continuous watching does not guarantee a fixed GPS update rate. Suitability and business preparation sample the latest coordinates separately, initially and about every 30 seconds while visible, so map movement does not trigger a provider call on every GPS reading. Browser permissions, power management and network conditions can delay or prevent a result. The reported accuracy is an estimate, not proof of the person’s true position.
Click-only business selection and retained-location recovery added in v52. Startup and periodic readings update the browsing origin and distance calculations; they do not create a default meetup pin. The separate current-location check classifies the device point to choose the map zoom and gate automatic business preparation. Once precise coordinates have been obtained during the current page session, a later failed reading retains them and displays “precise location unavailable - using previously obtained location” over the map. A successful reading clears that notice. The retained fix can become stale and is not proof that the visitor stayed there. It is held in page memory; a full reload begins a new acquisition. Before any precise fix, the browser can use a session-cached IP estimate or request one from api.ipapi.is. IP location is approximate, is not a meetup approval, and cannot silently replace a precise fix that arrives while the fallback is pending. IP-based city labels in server diagnostics are a separate fallback from the map’s browsing origin.
Prepared business selection added in v55. Automatic business preparation runs only after the current-location screen returns allowed. Unsuitable, unknown, checking and unavailable results suppress this lookup; explicit address searches and the user-activated nearby-business dropdown remain available. Preparation posts to /api/current-business, searches mapped shops and selected food, drink and entertainment venues within 50 metres, and considers the nearest returned business by its mapped node or center. This radius is fixed, not expanded using reported GPS accuracy. It tries that candidate rather than iterating through every nearby business. If the business is a mapped way with an associated public entrance=main node within the same radius, that entrance is checked first. Otherwise, or if the entrance fails eligibility, its mapped node/center is checked. A successful check must identify both an allowed point and a business. Mapped centers are provider coordinates, not guaranteed pedestrian entrances; entrance data can be absent or incomplete.
Current-location notice visibility refined in v68. The detected business name and current-location suitability notices are displayed beside the location controls only while no meeting point is selected. Selecting or restoring a meeting point hides these notices; clearing the selection shows the current notice again. The selected meeting-point status takes the same position above the nearby-business controls, replacing the current-location notice. Detailed diagnostics remain available, and background preparation and validation keep their existing behavior. “Use my location” copies its already prepared address and checked coordinates into the fixed meetup pin without starting a location, business or suitability lookup. Readiness is required: the button is disabled during discovery, after a no-business result or failure, and for IP-only/no location. A ready result remains usable with retained precise coordinates. Clicking again uses the prepared selection; later device readings do not move an existing pin. A delayed preparation result never selects a point automatically. Actual submission still validates meetup coordinates on the server.
The browser retains the most recent prepared business or no-business result in page memory for up to 24 hours within 5 metres of its lookup origin; movement beyond that tolerance or expiry prepares a new result at a later 30-second sampling step. This reuses the business’s checked canonical coordinates, not approval of a new raw device point. Failures retry on a later device update after at least 30 seconds. Obsolete requests are cancelled. Preparation is suspended unless current-location suitability is allowed and during an active match or locked wingfam. Retained device coordinates remain usable; the retained-location warning stays until a successful device reading. Preparation can involve fallback providers and entrance/center checks. It shares the raw-query cache infrastructure with the dropdown, but their different query shapes use separate entries; its suitability checks can reuse the server caches below.
Meeting-point eligibility and residential screening
Shared suitability caching introduced in v48. 24-hour results, geometry reuse and earlier provider fallback added in v51. Every point is classified separately, even when provider data is reused. Device coordinates can be available while a suitability lookup is unavailable; a provider outage is distinct from an authoritative rejection and never grants approval.
Introduced in v34.
Every new or updated request and wingfam reopening checks its coordinates on the server. Selection controls check them before accepting a new point, and restored draft/history locations are checked again before posting. The browser sends the point to /api/meetup-location; the server asks Overpass for containing mapped areas and nearby feature geometry. It considers land use, building footprints, access tags, adult-venue tags and public-facing venues. A nearby venue does not automatically validate an entire neighborhood.
A containing residential building or private-access area is rejected. A named eligible shop or public venue at the point can qualify within a broader residential land-use area; otherwise residential, agricultural, industrial and other unsuitable mapped land uses are rejected. A mapped commercial/retail area or building can qualify. Unclassified points are not accepted. A public venue represented by a map node must be within about 15 metres; mapped building footprints use point-in-polygon checks. These tolerances, incomplete geometry and mixed-use tagging can cause false positives or false negatives.
The required-point form overlay was added in v39. Until a meetup point is selected and checked, the request form is inert, its fields are disabled, and an overlay directs the visitor to the location controls. Suitability notice styling added in v65. Location messages use consistent notice panels: red for a failed or unavailable check; yellow for an unsuitable location, no suitable nearby business or a missing meeting point; green for a ready nearby business or a selected point whose eligibility check has completed successfully; and neutral while checking, locating or waiting. Dark mode uses corresponding red, amber, green and slate panels. A stale prepared business is not shown as a green result while its replacement is being prepared. Text explains the result independently of color. Green records an app eligibility or preparation result, not verified opening hours or guaranteed safety. Saved-request selection remains available above the form. Restored points are checked; a rejected or unavailable restored point leaves the form locked. A failed replacement selected through the location controls preserves the prior point. Selecting an eligible point does not itself post a request.
Provider recovery improved in v56. Suitability and business lookups use Private.coffee, overpass-api.de and VK Maps (maps.mail.ru), with a 10-second client timeout per provider. Suitability queries allow eight seconds of server execution. Fallbacks start after one and two seconds respectively, or the next starts immediately when an attempt fails; remaining lookups are cancelled once a complete response arrives. HTTP errors, timeout remarks and incomplete responses are not treated as suitability decisions. Only coordinates and query instructions are sent for this check, not names, photos or plan text. Classification is not a safety guarantee, opening-hours check, legal zoning determination or proof that a venue is open to visitors. It depends on Overpass query semantics and OpenStreetMap land-use tags.
Shared lookup ownership and waiting
Cross-Worker deduplication added in v58. An uncached suitability, text/reverse-address or raw-business query atomically acquires a 20-second D1 lease for its exact lookup key. The lease contains the key, a random per-attempt owner token and expiry, separate from cached decisions. Only the owner calls providers. Other requests recheck the result cache with delays beginning at 150 milliseconds and increasing to one second, plus up to 99 milliseconds of jitter. Waiting has a 12.5-second deadline. A waiter can acquire a released lease during its first 1.5 seconds; after that it only waits for a result or returns a retryable error, leaving time for provider work within the client deadline. Existing matching-action timeouts can end the browser wait earlier; server work may still finish and populate the cache.
The owner rechecks the cache after acquisition, publishes a completed result before releasing its lease, and releases on failure without caching the error. Cache writes require the current, unexpired owner token; a late owner cannot overwrite a replacement’s result or delete its lease. A crashed owner stops blocking acquisition after 20 seconds, and a later request can recover the expired lease. Expired lease rows are removed by bounded shared background cleanup; no independent cleanup timer runs. A coordination-storage outage falls back to the existing per-Worker sharing and provider path, so duplicate work remains possible during that outage. Lease expiry also permits temporary overlap with an unusually slow old owner. Leases reduce duplicate work; they do not guarantee exactly-once provider execution. Exact-coordinate suitability boundaries and original result expiry remain unchanged.
Four separate lookup caches
- Address and business-name search
- Shared text-search caching added in v56. Photon search results are stored in D1 and up to 500 Worker-memory entries. Text keys normalize Unicode, letter case and whitespace while preserving the rest of the query, including city names. Reverse-address keys use exact coordinates. Successful nonempty results last 24 hours; valid empty results last 30 seconds. Concurrent equivalent searches share work within a Worker and use database leases across Workers. Errors and malformed provider responses are not cached. Selecting a returned place still checks its exact coordinates using the separate suitability cache; a name alone never grants eligibility. Fresh cache reads preserve the original expiry. Expired rows are removed by shared background cleanup, without a separate timer or row eviction cap; storage is finite.
- Exact-point suitability
- D1 stores both allowed and rejected results for 24 hours, including the business flag, reason and expiry. Keys use exact coordinates and a rule version. Concurrent checks of the same point share in-flight work within a Worker and use database leases across Workers. Up to 500 classifications are held in Worker memory; memory eviction can fall back to the shared database.
- Nearby map geometry
- A separate D1 cache stores reusable geometry, source coordinates and expiry for 24 hours, with up to 100 records in Worker memory. A selected point within 5 metres can reuse suitable geometry but is classified anew. Unsupported or missing area boundaries prevent reuse. The supported path requires closed way geometry for containing areas and declines relevant relation geometry. Derived classifications keep the original source expiry, so reuse does not extend data age.
- Raw business queries
- Shared query caching and deduplication added in v58. Both the nearby-business dropdown and current-business preparation use a D1 result cache keyed by a SHA-256 hash of the complete Overpass query, including its coordinates, radius, filters and output requirements. Different queries remain separate. Valid empty-result caching extended in v60. Complete results, including valid empty results, are reused for 24 hours, with up to 100 query results in Worker memory. Concurrent identical queries share work locally and use database leases across Workers. Errors and incomplete responses are not cached. Expired rows are ignored and removed by shared background cleanup, without a row eviction cap or independent cleanup timer; storage is finite. This replaces the dropdown’s former edge cache. The result is map data, not approval of a business meetup point. The browser reuses lists for two minutes. Opening the dropdown, changing its distance or later center changes after activation can fetch a list. After failure it schedules another attempt about every two minutes while enabled; browser suspension can delay that retry. The prepared Use my location selection uses a different query and still validates the chosen meetup coordinates.
The shared suitability and geometry caches have no 5,000-row eviction cap; storage capacity is still finite. Expired entries stop being reused and are physically removed during shared background cleanup, not by a separate deletion timer. Cache failure can fall back to provider checks. Coordinates may describe sensitive places even without visitor/session identifiers, and stale map data may remain reusable until expiry. The 5-metre reuse distance is not GPS smoothing, and map boundaries remain only as reliable as the source data. Current address-search and business responses use no-store instructions. The separate city-name and city-center caches have the lifetimes described in the visitor section.
Meeting times and travel radius
Form contract and time normalization
Relative timing added in v37.
The required timing choice is a radio group: specify a time, or use “Now / travel time”. Scheduled appointments retain the 15-minute controls and 12-hour limit. Relative requests replace the clock dropdowns with a minutes-from-now dropdown with “Now” followed by five-minute increments whose starting value and upper limit depend on distance and the requester’s selected transportation mode. The zero-valued option is labeled “Now” and adds no delay or grace; other values display the chosen number of minutes from now throughout request cards, maps and match details. This lets visitors allow travel time without maintaining a fixed appointment. Relative requests do not auto-advance. The API stores the reserved value meet_at = 0 for relative availability rather than an appointment timestamp, together with meet_delay_minutes. The server validates five-minute increments, the distance-based allowance and the 12-hour maximum, and normalizes the auto-advance interval to zero. Matching evaluates the relative value as the current time plus the selected delay, applying the existing one-hour compatibility window to both participants. When both participants finalize, the server atomically records a fixed final_meet_at for the selected meeting plan. If both requests use relative timing, it uses the larger travel allowance, regardless of which meeting location was selected: 10 and 25 minutes become 25 minutes after finalization. If only one request is relative, the selected meeting plan supplies the time. Later heartbeats and phone exchange updates preserve it. A changed plan or withdrawn readiness clears it so a new finalization can establish a fresh time.
Transportation and the travel allowance
Timing layout refined in v68. Reset form sits just below the timing and transportation controls, with a separate gap before the compact, multiline Now / travel time explanation.
The separate grace allowance was removed in v40.
Control order and Already there added in v39. The minutes dropdown appears before transportation. Selecting “Now” automatically selects “Already there,” which has zero travel allowance; selecting “Already there” also returns the duration to “Now.” Nonzero durations use walking, cycling, driving/rideshare or public transit, with driving as the initial mode when leaving “Now.” Distance-based starting durations and two extra upper choices added in v46. Both the form and posting API calculate a range from straight-line distance between the supplied detected location and meetup point. The first nonzero choice estimates best-case travel at 20 minutes per mile for walking, 4 for cycling, 1.5 for driving/rideshare and 3 for public transit; it rounds that estimate up to five minutes and then adds five minutes. The upper heuristic uses 25, 8, 5 and 12 minutes per mile respectively, rounds up to five minutes with a ten-minute floor, and adds two more five-minute increments. Both bounds are capped at 720 minutes. These additions shape the dropdown options; finalization adds nothing beyond the duration actually selected. The overall cap is 720 minutes. The cap limits the available choices; nothing is added to the selected delay. “Now” remains zero, and two people choosing “Now” finalize for the current time. These are product estimates with generous allowances, not route calculations, traffic predictions or transit timetables.
The dropdown shows Now and values within the calculated range; changing the mode or meetup location can adjust an unsaved duration up or down. Existing live requests retain their server value until updated. The server independently rejects nonzero values below the minimum or above the maximum on create/update, and wingfam reopening clamps a reused nonzero allowance to the range for its confirmed point. Detected coordinates are already supplied for the existing 50-mile location check; this calculation does not add a routing provider or a stored journey origin. Saved requests retain the selected mode and delay. A selected cap or travel estimate is not a promise that any route can be completed within that time.
Relative availability has no appointment-age expiry. It still uses the ordinary server session: heartbeat renewal keeps the request live, withdrawal removes it, and 90 seconds without renewal makes it expire. Closing one tab does not end a session still being maintained by another coordinated tab. Server cleanup excludes the immediate marker from the one-hour-past-appointment rule while retaining session expiry and completed-match cleanup. Saved request reuse and wingfam reopening preserve the relative choice and transportation mode; the travel allowance is capped again when a wingfam is reopened at a checked point.
Required markers and Prefer not to say were added in v32.
Required request controls are marked with an asterisk, with a legend near the bottom of the form. Name, adult age, a gender response (including “Prefer not to say”), matching preference, plan, meeting time, meeting spot and radius are required. A photo and automatic time advancement are optional. Browser controls provide immediate feedback and the server schema independently validates submitted values.
Scheduled times are entered in 15-minute increments with AM/PM controls and stored as absolute timestamps. The posting API requires the absolute timestamp to fall between the current server time and 12 hours ahead, with no 6 AM cutoff or separate today/tomorrow restriction. The browser interprets a selected clock time as its next local occurrence, advancing to tomorrow when that time has already passed today. Display formatting uses the visitor’s local time. The former tomorrow-before-6-AM restriction was removed in v34.
Auto-advance added in v10.
Optional auto-advance moves a passed meeting time forward in 15-, 30-, 45- or 60-minute steps. Live requests advance through server-side activity, and draft controls can advance in the browser. Automatic changes pause while a request is participating in a wingmatch.
Asymmetric travel constraints
Travel radii range from a quarter mile to 50 miles. Asymmetric radii matter: if one person is willing to travel farther, they may meet at the other person’s spot even if the reverse journey is outside the other person’s radius. The server validates the specific selected place, not just the distance between the two people. Posting also requires a chosen location within 50 miles of the location supplied by the browser’s detection flow.
Matching and mutual agreement
Mutual matching launched in v1. Asymmetric travel support added in v4.
Nearby discovery and the map are filtered views of live requests. Compatibility requires mutual gender preferences, meeting times within one hour of one another (relative availability is evaluated against the current time plus its travel allowance), and at least one meeting location that the relevant visitor can reach within their chosen radius. Distance uses a great-circle calculation over latitude and longitude; it is not driving distance or a route-time estimate.
Gender preferences for comfort and personal safety
Mutual gender preferences launched in v1.
The “Looking for” control, called desired in the request data, exists so people can choose company of genders they feel safe and comfortable around. For example, a woman may want a female wingwoman, a nonbinary person may want to meet another nonbinary person, or a man may be looking for a “guys’ night out.” It expresses a preference for a platonic companion, not romantic or sexual intent.
The server applies this preference in both directions: each person must meet the other’s stated preference, or that person must have selected “Anyone.” One visitor’s preference never overrides another’s. Gender is self-reported; this filter does not verify identity or guarantee personal safety.
The default gender became “Prefer not to say” in v38. A fresh or reset form uses this choice; a valid saved response is restored. Choosing “Prefer not to say” is a valid gender response without revealing a gender. It does not satisfy a specific-gender preference; another visitor must choose “Anyone” for that side of the mutual preference check. The visitor who declines disclosure can still choose their own preference.
Sending an invitation creates a match and participant records in a database batch. A unique participant key prevents a request from joining two matches at once. If two invitations compete, a conflicting attempt is rejected rather than silently assigning both.
Match state machine
Recognition choices and the mutual-search conflict
Introduced in v34.
After agreeing on the location, each participant chooses a free-form description, “I’ll find you,” or “Meet outside the front door.” The front-door option requires a mapped named business at the agreed point; being somewhere in a commercial district is insufficient. The browser enables it only after checking that point, and the server checks again when saving that choice and when either participant finalizes.
Recognition mode is stored separately from description text in the match record, with older records defaulting to written description. Presets produce a fixed recognition instruction in the final exchange. The partner learns whether someone chose “I’ll find you” through the normal snapshot before finalization; private written recognition details continue to appear in the final exchange.
If both select “I’ll find you,” both pages show that they are both trying to stay safe and that one must choose a different option. The server rejects readiness while that conflict exists, even if a client bypasses the disabled button. Changing recognition mode or details resets both ready flags and increments the plan revision. A front-door choice does not establish which entrance is accessible or that the business is open.
Optimistic concurrency and mutual consent
Match records have revision numbers. Updates are conditional on the version the server read; finalization also checks the plan revision reviewed by the user. Changes to the place or recognition details reset readiness. These checks help prevent a stale screen from confirming a plan that changed underneath it. The API verifies active participants and live expiry on relevant actions.
Optional phone exchange
The finalized-view phone exchange was added in v12.
Phone exchange is optional. Numbers are parsed and validated, and the final exchange reveals them only when both people have entered a number. This is not end-to-end encrypted messaging: the service processes the details to coordinate the exchange.
Wingfam reopening
Introduced in v16.
A wingfam lets a participant reopen availability at the confirmed place after a finalized match. A server permission renewed for 90 seconds while the finalized snapshot is read preserves the relevant request information for that transition. Reopening requires an explicit location confirmation, a fresh eligibility check or valid cache result, and a location within 50 miles of the supplied browsing origin. The wingfam host’s address is fixed; guests must be able to travel to it. Completing an ordinary wingmatch removes ordinary matched requests while an active wingfam host can remain available.
Data lifecycle and recovery
What is stored where
- Page memory
- Current edits, request and match snapshots, provider and lookup logs, in-flight work, and the freshly generated fallback session token.
- HttpOnly cookie
- The temporary capability used to recover and control a live request on the same site.
- Local storage
- Selected draft/profile details including plan text, the processed profile photo, up to five previous requests, sound and appearance preferences, saved map center/zoom, short-lived visitor presence, temporary finalized-match recovery data, dismissal records, photo association and tab-coordination markers. Clearing site data removes these conveniences.
- Session storage
- A temporary approximate IP-location result can be reused within the tab’s session to avoid repeated lookup requests.
- D1 database
- Shared request and match state, session-token hashes, the latest browser-reported request activity timestamp, coordination constraints, visitor-presence hashes, location, temporary request-session association and expiry, operational metadata, city lookup results, meetup classifications, address/business-name search results, reusable map geometry, raw business-query results, short-lived lookup leases and removal receipts.
- R2 bucket
- Photo files and their smaller display versions.
Browser storage can outlive the live request. It is not a private account vault, and another person using the same browser profile may see saved information. The Privacy Policy describes public fields, provider processing and retention in more detail.
Previous requests and finalized-copy recovery
Deduplication and draft reuse
History introduced in v13. Deduplication and whole-entry reuse refined in v32.
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. Saved meeting-point restoration protected in v48. 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 control repositioned in v65. 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
Local recovery added in v11. Persistent dismissal and stale-copy protection refined in v31.
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
After 90 seconds without a successful server renewal, a request stops qualifying for live API results. Already-loaded screens may still show their old snapshot until they refresh. Leaving through the app requests immediate removal of the live request.
Database cleanup runs in the background after a matching, meetup-check, business-search or place-search API response, using the hosting runtime’s supported after-response lifetime. Background cleanup and a shared database lease added in v57. Live database views exclude expired requests, overdue scheduled plans and retired ordinary finalized pairs even while their rows remain stored. Expired match membership does not make a live request appear busy. Due automatic time advances remain part of live processing; eligible unmatched auto-advancing plans are not retired merely because cleanup runs before their next advance.
A single shared lease is claimed atomically in D1 before housekeeping begins; competing Worker instances skip the sweep. The claim is also deferred, so the response does not wait for it. Successful sweeps have a 30-second cooldown, and each application instance throttles its attempts. The lease lasts 120 seconds to recover from an interrupted invocation; ownership checks prevent an old job from releasing a newer lease. Each pass selects at most 200 expired requests, also retiring their ordinary finalized partners and cascading dependent records, and removes up to 200 expired rows from each visitor, rate-limit, wingfam-permit, geography, city-center, place-search, meetup-classification and reusable-map-data table. It then attempts up to 20 unused photo assets, including their display variants. The job stops starting new stages after 20 seconds; Cloudflare may cancel unfinished background work after its 30-second post-response allowance. Failures are logged and later traffic retries. Large backlogs take multiple passes.
There is no independent scheduled cleanup service: without later matching, meetup-check, business-search or place-search traffic, expired data can remain stored. Background work still shares database and hosting resources with live requests, so this removes the direct wait for cleanup rather than guaranteeing zero contention. Small, targeted operations needed for correctness remain in the live flow: releasing an expired session’s unique slot for a new post, releasing stale membership for a new invitation, explicit withdrawal, match completion and visitor-requested photo removal. Cache lookups and cache writes remain awaited; the broad expired-cache sweeps are deferred.
Photo cleanup also runs in background batches on later activity. Provider backups, logs, browser storage and removal receipts have separate lifetimes. The 90-second availability rule is not a promise that every copy of every piece of data disappears within 90 seconds.
Photo processing, access and visitor removal
Upload pipeline
Request photos introduced in v14. Server resizing expanded in v18. The 3.5 MB input limit was added in v19.
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
Introduced in v31.
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
Introduced in v30. Drug screening added in v50.
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 v30 changelog records the banned emoji inventory introduced with screening.
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
Interactive diagnostics introduced in v44. Suitability and device-reading information added in v52. Collapsible sections added in v53. The masthead badge opens a blurred panel fixed to the upper-right viewport corner. It closes with its close button or Escape and returns keyboard focus to the badge. The panel is 330 pixels wide where space allows and grows with its content up to the available viewport height, then scrolls internally.
All sections are individually collapsible. Live counts opens expanded by default from v60. Live counts and Connection open expanded; Your session, Meeting-place suitability, Browser details & timing notes, and Log start collapsed each time the panel is opened. Expanding one section does not require closing another.
The panel shows existing counts, heartbeat status and attempts, snapshot age, API round-trip duration, request lease countdown, match phase, coordinates and city-resolution information. Suitability diagnostics show the last client check/selection status, its explanation and age, the current selected pin, the separate current-location map-check status, reported device accuracy and reading age, and the configured selection/cache policy. The last check can describe a failed attempt while an older selected point remains. Cache policy is configuration, not proof that a particular lookup was a cache hit. The panel does not inspect cache rows or independently recheck a location.
Round-trip timing includes matching-server processing and JSON handling. The lease countdown combines server snapshot time with elapsed time since receipt. Stale counts are labeled. A one-second display timer runs only while the panel is open; opening or expanding it adds no provider requests, telemetry or persistent diagnostic records.
Location diagnostics clarified in v70. The two search-area count rows use matching icon and text columns so wrapped lines retain their alignment. The selected-location details show distance from your current location and omit the redundant distance from the selected meeting point to itself. Your session explicitly reports whether the approximate IP area is the active current location. The last IP attempt is shown separately, with fresh-provider versus tab sessionStorage provenance, original result time and cache reuse deadline. A fresh result reports whether saving to sessionStorage succeeded. Expiry stops future cache reuse but does not erase an already displayed area or force a lookup; device acquisition takes precedence. A delayed IP response is reported as not applied when device location has arrived. The page log records fallback starts, cache hits or misses, fresh successes, failures, activation and replacement by device coordinates. These entries contain no IP address or coordinates. API labels use one-word purposes such as Location, Geocoding and Mapping followed by the provider hostname in brackets; coordination categories use this site’s hostname because cached work may have no provider request. The separate active flag is authoritative for current use; historical log entries and last-attempt fields remain observations of earlier work.
Separate suitability cache policies added in v66. The meeting-place diagnostics distinguish meeting-point suitability (up to 24 hours for exact coordinates), map geometry (up to 24 hours, with nearby reuse within 5 meters and separate classification of each point), and current-location suitability (browser-memory reuse for up to 24 hours within 5 meters, with a 30-second cooldown after failed checks). A decision using cached geometry inherits its expiry; reuse does not extend it. These policy descriptions are configuration, while meeting-point and current-location result-source fields describe observed checks. No separate geometry-source field is displayed because exact-point cached decisions do not preserve that provenance. Diagnostic distance units spell out meters.
City cache labels clarified in v63. Your session separates the city’s location basis from its resolution method. Method labels explicitly identify server-memory cache, shared database cache, fresh provider lookup, shared in-flight work or hosting IP geography. Cached unsuccessful results identify the server layer and a retry cooldown of up to 30 seconds. These describe the last server snapshot, not a live provider request. No separate browser city-resolution cache is used. The static Browser city cache and Persistent browser cache rows were removed in v64. Server memory reuses city results for up to five minutes and shared database successes last 24 hours. The tab’s separate one-hour sessionStorage cache stores an approximate IP browsing area; it is not the city-resolution cache.
Suitability provenance added in v61. The suitability section separates automatic device screening from selected-meeting-point validation. Each reports its own check time and reason; an unsuitable device does not imply a selected-point check occurred. Device diagnostics identify browser-memory reuse, the original server source, result reuse deadline and original map-data time when supplied. Server responses distinguish exact-point Worker memory, shared D1 results, reused geometry and fresh provider work. In-flight shared results are identified separately; their timestamps may be unavailable if persistence failed. Browser device screening reuses results within five metres for up to 24 hours from receipt, or waits 30 seconds after an unavailable check before a later location reading can retry. These browser and server expiries are distinct. A paused screen sends no request. Suitability is not persisted in localStorage or the HTTP browser cache. Selected points are validated independently; Use my location can select a previously prepared business from page memory. Restored draft/history coordinates do not restore a suitability approval. One-second local display updates restored in v65. The Connection section distinguishes its one-second display update from its ten-second heartbeat target; display ticks send no request. Location, map-bound and radius changes are included in the next ten-second background poll, rather than scheduling extra requests for every GPS reading. Explicit actions, cross-tab changes and page restoration can still trigger additional refreshes.
Provider failure log added in v56. Log scrolling and spacing improved in v68. The log box starts at 180 pixels tall, scrolls vertically within the diagnostics panel, and can be resized between 120 and 360 pixels. Entries follow each other without an inserted blank line; trailing whitespace is removed while line breaks inside provider messages are retained. The read-only Log textarea shows UTC timestamps, one-word API purposes with hostnames in brackets, HTTP status codes and provider response text, including failed attempts before a successful fallback. Network failures without an HTTP response are labeled accordingly. HTTP 200 timeout remarks and malformed JSON are logged as provider failures. Response output is limited to 4,096 characters per attempt, at most 20 attempts per server response and the latest 100 entries in page memory. Lookup coordination events added in v58. Cache hits, lookup starts, waits, expired-lease recovery, wait deadlines and coordination-storage fallback are also recorded, labeled as coordination rather than an HTTP status. Provider failures take priority over coordination entries within the 20-entry response limit. Coordination labels identify the lookup category without including coordinates, search text or owner tokens. Requests carry their own failure records; no shared visitor log or persistent diagnostic database is created; the operational lease table is separate from these logs. Concurrent shared work is attributed to the request that initiated it. Cancelled losing fallback attempts are omitted. Query strings are excluded from provider labels, but provider output can echo searched locations. Logs survive closing the panel and clear on reload. A lost or cancelled browser response may prevent its server-side failure records from reaching this page. The log observes fetch-based providers, not map-tile or script resource loads.
Appearance preferences
Light, dark and automatic appearance added in v46. A header icon cycles from Auto to Light to Dark and back to Auto. Auto is the default and follows the device colour-scheme media query, including changes while the page is open. The browser stores the selected mode in local storage under wingman-theme; the preference is not sent to the server. A small script applies the mode before page content is displayed, listens for device changes and synchronizes ordinary tabs through storage events. Policy pages and the standalone changelog use the same setting. If browser storage is unavailable, switching still works for the current page but the preference cannot survive a reload. Dark mode uses charcoal/slate surfaces and keeps blue and yellow accents.
Active request form
The update-mode marquee was added in v46. 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. Conditional lightning fill corrected in v47. 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.
In-page alerts, sound and haptics
Sound cues introduced in v25. Speaker animation added in v27. Haptics and supported media playback added in v28.
Wingman uses in-page toast notifications and sound cues for relevant toast and match-state transitions. The chimes are small first-party PCM audio files played through one reusable HTML audio element. The first interaction primes that element with a silent file; Test sound calls media playback directly from the tap, without awaiting an AudioContext or delaying the test in a timer. Playback rejection is handled and a failed test opens the sound help. The speaker animation follows media playback events. Media-element playback replaced Web Audio chimes in v41. Cues are prioritized and briefly coalesced to avoid an overlapping burst, and browser interaction may be needed to unlock audio.
Sounds default to on, with the preference saved locally. Where the browser supports the Audio Session API, playback requests the media-playback category while sounds are enabled; that can allow sound in situations where ordinary alert audio would be muted. Media volume and platform behavior still apply. The previous audio-session category is restored when sounds are disabled or the component is removed. A running media element does not prove that the physical speaker is audible.
On browsers that support it, the Vibration API supplies short haptic patterns after interaction while the page is visible. Haptics are independent of the sound switch. “Test sound” tests both. Unsupported browsers simply omit vibration. Wingman currently does not request native notification permission, register push subscriptions, or send background browser notifications.
Installation and offline behavior
Introduced in v2.
The site can be added to a device’s home screen on supporting browsers. 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
Version badge and changelog introduced in v12. Optional refresh banner added in v23. Banner changelog link added in v30.
The version badge and changelog come from the release history. While the page is visible, an update check normally runs every 30 seconds and on relevant return or connection events. If the server reports a newer version, a banner offers a refresh and a link to the latest improvements. It does not force a refresh in the middle of a request. Content-based static-asset URLs added in v49. The build hashes the bytes of first-party theme styles and scripts, document-refresh code, sound files and installation icons, and adds that hash to each asset URL. An asset URL changes only when its contents change; release numbers alone do not invalidate unchanged files. The installation manifest references hashed icons and gets its own content hash. The homepage, document shell and standalone changelog use the same generated asset map. Framework JavaScript and CSS already use build-generated hashed filenames. Clicking the update button performs a normal page reload, whose new asset references bypass older cached file URLs. This does not clear local storage, cookies, saved requests or browser caches.
Releases build the application and bundle its database migrations before publication. This explainer is maintained alongside the code: project instructions require agents to review it before publishing and update it when implementation changes affect the descriptions here.
How it’s Built
This development-process account was added in v35.
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 v34 removed the cutoff and tested the rolling 12-hour window. 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.
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.
Documentation and policy accuracy
Changes to sessions, synchronization, matching, photos, location, storage or other described behavior require a review of this technical document. Diagrams and linked introduction/change versions must remain accurate. Changes to collection, sharing, retention or user controls also 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 automatically advance them.
Consistent navigation and honest limits
Footer and policy links open in a new tab. Section anchors and the sitemap must remain usable. Documentation distinguishes browser convenience data from authoritative server state and avoids unsupported promises about identity, delivery, moderation or deletion. These instructions capture recurring lessons so they do not depend entirely on the memory of one conversation.
Examples from the earlier build
Live status needed database evidence
Early feedback questioned whether a request was truly live. v6 required database confirmation before displaying that status, and v7 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 v15. A later report reproduced a failing lookup on the published site. v17 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 in v19 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. v22 corrected 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?
Added in v37.
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.
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.