From 41096e8af1ce9d68766c886e3653975541c7a750 Mon Sep 17 00:00:00 2001 From: Emmanuel Jones Date: Sat, 8 Aug 2026 13:55:07 -0600 Subject: [PATCH] Document Expo migration plan --- docs/expo-migration-rfc.md | 384 +++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 docs/expo-migration-rfc.md diff --git a/docs/expo-migration-rfc.md b/docs/expo-migration-rfc.md new file mode 100644 index 0000000..3d007d2 --- /dev/null +++ b/docs/expo-migration-rfc.md @@ -0,0 +1,384 @@ +# Expo Migration RFC + +- Status: Accepted for Phase 0 +- Date: 2026-08-08 +- Owners: RCV maintainers +- Decision horizon: architecture and first product milestone + +## Summary + +Build a new Expo/React Native client alongside the existing AngularJS app. The +new client will initially reuse the PHP/MySQL backend and preserve the current +website in production. Migration will proceed through vertical product slices, +starting with anonymous ballot lookup, voting, and results. + +Do not begin with account management or owner-only ballot mutations. The +current browser client persists a user ID and name in cookies, while many PHP +endpoints trust caller-supplied user or `createdBy` fields. A native client must +not carry that implicit trust forward. Authenticated features will be added +only after the backend can issue and verify tokens and hash passwords on the +server. + +## Why now + +The AngularJS application is usable and has meaningful regression coverage, +but most UI behavior remains concentrated in `MainCtrl`, `ballot.js`, and +inline page fragments. Additional broad tests would primarily preserve an +interface intended for retirement. The migration should instead use the +existing tests as a characterization harness and invest new coverage at the +boundaries reused by Expo. + +The product's defining constraint remains the 15-second ballot: a person must +be able to create or open a ballot and vote without first learning an account +or setup workflow. + +## Goals + +- Ship one codebase for iOS and Android, with a credible path to web. +- Keep the PHP/MySQL backend during the initial migration. +- Preserve existing ballot shortcodes and shareable links. +- Preserve anonymous voting and the fast default experience. +- Move reusable election and formatting logic into framework-neutral + TypeScript. +- Replace implicit client identity with explicit server-verified authorization + before porting account and ballot-management features. +- Migrate incrementally without interrupting the production AngularJS site. + +## Non-goals + +- Rewriting the PHP backend and database at the same time as the client. +- Reaching complete feature parity in the first release. +- Supporting offline vote submission. +- Porting Bootstrap/jQuery components or reproducing the existing DOM exactly. +- Adding speculative session-authentication tests to the current endpoints. +- Making Expo web the production website before dynamic-route, SEO, and hosting + behavior have been proven. + +## Current-system inventory + +### User-facing areas + +| Area | Current implementation | Migration priority | +|---|---|---| +| Home and shortcode entry | `home.html`, path-as-shortcode routing | Milestone 1 | +| Vote | `vote.html`, sortable candidate list | Milestone 1 | +| Results and round detail | `results.html`, `results-detail.html`, `VoteFactory` | Milestone 1 | +| Create ballot | `create.html`, multi-step state in `ballot.js` | Milestone 2 | +| Secure voter codes | `code.html`, validation and management endpoints | Milestone 2 | +| Registration and login | `register.html`, `auth.js` | Milestone 3 | +| Profile and ballot management | `profile.html`, `manage.html` | Milestone 3 | +| Group questions | create, vote, results, CSV export | Milestone 4 | +| RCVis integration | browser iframe plus cURL PHP endpoints | Milestone 4 | +| Custom HTML/iframe content | browser-specific rendering and editing | Web-only until reviewed | +| Admin, hall of fame, static content | separate/low-use surfaces | Later or remain web-only | + +### Backend and data + +The existing PHP API contains roughly 45 endpoint scripts over nine production +tables: ballots, entries, votes, users, random codes, ballot-code assignments, +group fields/options, and contributions. It already supports the core +anonymous flow: + +1. `get-candidates.php` returns a ballot, candidates, and group fields. +2. `vote.php` accepts the ordered names/IDs plus voter metadata. +3. `get-votes.php` returns ballot metadata, entries, and vote rows. + +The live MySQL contract suite exercises a high-signal create -> entries -> vote +-> results sequence. PHPUnit provides broader endpoint characterization using +SQLite. + +### Reusable JavaScript + +- `src/js/factories/vote-factory.js`: election rounds, transfers, tie breaks, + and RCVis payload construction. Its algorithm coverage is the strongest + candidate for extraction. +- `src/js/utils/borda.js`: framework-neutral Borda calculation. +- `src/js/utils/rcvis-helpers.js`: framework-neutral visibility/update rules. +- Parts of `src/js/ballot.js`: date, title, slug, and group-field helpers. + +`VoteFactory` still depends on Angular scope, Lodash globals, and network side +effects. Extraction should separate pure election calculation from display and +RCVis synchronization; it should not be copied wholesale into Expo. + +### Current authentication boundary + +- Login compares a client-generated legacy hash directly with the `users` + table. +- The browser stores user ID, name, and clearance in readable cookies. +- The backend does not issue an ordinary-user session or access token. +- Multiple reads and mutations accept user identity or `createdBy` from the + request. +- `PASSWORD_MIGRATION_PLAN.md` already identifies the need for server-side + password hashing. + +This is known security debt. It is also the reason authenticated native +features are deferred until Milestone 3. + +## Decisions + +### 1. Add Expo alongside the existing app + +Create `apps/mobile/` and leave the current root Vite build intact. The first +scaffold should have its own commands and must not alter production deployment. +Repository workspace consolidation can follow after both toolchains run +reliably in CI. + +### 2. Use TypeScript and Expo Router + +Use file-based routes for native and future web navigation. Expo recommends +Expo Router for new universal applications and provides automatic deep-link +handling and typed-route support: + +- +- + +Proposed initial routes: + +```text +src/app/ + _layout.tsx + index.tsx # shortcode lookup + ballot/[key]/index.tsx # ballot details and ranking + ballot/[key]/thanks.tsx + ballot/[key]/results.tsx +``` + +Do not mirror every Angular navigation item. Routes should follow user tasks. + +### 3. Preserve links through a compatibility layer + +The canonical future route is `https://rankedchoices.com/ballot/`. During +migration, existing `https://rankedchoices.com/` links must continue to +work and resolve to the same ballot. + +Use iOS Universal Links and Android App Links once a development build exists. +The domain association files remain a deployment task, not part of the initial +scaffold. Expo's documentation notes that domain verification is required and +that an HTTP(S) link falls back to the website if the app is absent: +. + +### 4. Keep the backend, add a versioned contract incrementally + +The Expo spike may read the existing endpoints through a typed adapter. New or +changed contracts should be introduced under `/api/v2/` and should provide: + +- consistent JSON envelopes; +- meaningful HTTP status codes; +- stable field names and JSON-native booleans/numbers; +- request IDs and safe error codes; +- explicit CORS policy for development and approved production origins; +- authorization middleware for protected routes; +- compatibility tests against MySQL. + +Do not rewrite every PHP endpoint first. Add v2 endpoints as a migrated product +slice needs them, delegating to shared PHP services where practical. + +### 5. Use API-issued tokens for native authentication + +Target a short-lived bearer access token plus a revocable, rotating refresh +token. Store native secrets with Expo SecureStore, which provides encrypted +device key-value storage: . + +Before authenticated Expo features ship: + +- hash new passwords server-side with PHP's password APIs; +- define the legacy-password migration or reset policy; +- add token/session tables with hashed refresh-token material; +- authorize every protected resource from the verified principal, never from + a request-supplied user ID; +- add owner/non-owner/anonymous contract tests; +- define revocation, expiry, device loss, and account-deletion behavior; +- separately decide browser credential handling before replacing authenticated + web screens. + +Token format is deliberately undecided. Opaque access tokens reduce accidental +data exposure and make immediate revocation straightforward; signed tokens may +be chosen only if stateless verification is a demonstrated requirement. + +### 6. Do not support offline vote submission + +The client may cache previously fetched public ballot data and results for +resilience. A vote requires an online server response because cutoffs, secure +codes, duplicate-vote rules, and current ballot configuration are authoritative +on the backend. Failed submissions remain visibly pending only long enough for +an explicit user retry; they are not silently queued. + +### 7. Extract a framework-neutral election package + +Create `packages/rcv-core/` only when the results slice begins. Port the tested +algorithm to TypeScript behind explicit inputs and outputs. No DOM, Angular +scope, jQuery, HTTP, RCVis calls, clocks, or global randomness may live in the +core package. + +Run the existing election fixtures against both implementations until the old +client is retired. Differences must be either fixed or recorded as an approved +behavior correction. + +### 8. Defer the Expo-web cutover decision + +The native app and production AngularJS site will coexist initially. Expo +Router supports universal routing and web output, but static web builds require +special handling for dynamic routes such as arbitrary ballot shortcodes. The +official static-rendering guide calls out that dynamic routes are not generated +arbitrarily: . + +Before web cutover, prove: + +- direct loading and refresh of arbitrary ballot URLs; +- search metadata and public-page indexing; +- legacy shortcode redirects; +- custom HTML/iframe behavior; +- printing and downloadable results; +- domain association files and PHP API hosting on the same deployment. + +## Target architecture + +```text +AngularJS web (temporary) ----\ + >---- PHP API /api + /api/v2 ---- MySQL +Expo iOS/Android -------------/ + | + +---- typed API adapter + +---- screen/application state + +---- packages/rcv-core (pure TypeScript) +``` + +The API is the migration seam. The legacy web app continues using `/api` while +Expo begins with adapters for compatible reads and adopts `/api/v2` as slices +need safer contracts. + +## Migration phases + +### Phase 0 — scaffold and connectivity + +- Add an isolated TypeScript Expo Router app. +- Configure development API base URLs without committing secrets. +- Implement shortcode lookup and read-only ballot detail using + `get-candidates.php`. +- Normalize the legacy response in one typed adapter. +- Document simulator, device, and local-PHP networking. +- Add adapter unit tests and a CI typecheck/test job. + +Exit criteria: iOS and Android development builds can display the same public +ballot from a local or staging PHP backend; the production web build is +unchanged. + +### Phase 1 — anonymous vote and results + +- Build an accessible ranking interaction with explicit move-up/move-down and + reset controls; gestures are an enhancement, not the only control. +- Submit votes online with clear loading, retry, duplicate, secure-code, and + cutoff states. +- Extract `rcv-core` and render local round results. +- Support the canonical `/ballot/[key]` route and test incoming links in a + development build. +- Cover the flow with API contract tests and one device-level E2E scenario. + +Exit criteria: open link -> rank -> submit -> results works anonymously on iOS +and Android, including failure recovery, without regressing the website. + +### Phase 2 — ballot creation and secure voting + +- Port the simple 15-second create flow first. +- Add optional settings progressively, not as an initial wizard. +- Define guest-ballot recovery/claim behavior. +- Add secure voter-code entry and validation. +- Add grouping questions needed during voting. +- Generate share links and native share-sheet content. + +Exit criteria: a guest can create and share a basic ballot; secure ballots can +be voted from native when supplied a valid code. + +### Phase 3 — authentication and management + +- Implement server-side password hashing and the chosen legacy-user migration. +- Add token issue, refresh, rotation, and revocation endpoints. +- Store native refresh credentials in SecureStore. +- Port registration, login, profile, claim, edit, transfer, reset, and delete + flows only after authorization tests pass. +- Add an explicit authorization matrix for every protected endpoint. + +Exit criteria: the server derives ownership from verified credentials and a +logged-in user can safely manage only their own ballots. + +### Phase 4 — advanced parity and web evaluation + +- RCVis display/synchronization. +- Full grouping management and exports. +- Borda views, custom entries, delayed results, and administrative tools. +- Decide which browser-specific features remain web-only. +- Run the Expo-web proof and decide whether, when, and how to replace AngularJS. + +### Phase 5 — retirement + +- Route eligible traffic to the new client. +- Observe errors, vote completion, and link-open success during a defined + overlap period. +- Retire AngularJS only after parity requirements, rollback steps, and data/API + compatibility are signed off. + +## Testing policy during migration + +Continue running the existing suites. Add new tests only where they protect a +migration seam or an active slice: + +- retain current Vitest algorithm/helper coverage; +- retain PHPUnit endpoint characterization and schema drift checks; +- retain the live MySQL API contract flow; +- retain the legacy Playwright smoke flows while AngularJS is in production; +- add TypeScript adapter and `rcv-core` unit tests; +- add v2 contract tests for every new endpoint; +- add a small number of device E2E flows at milestone boundaries. + +Paused unless a migration slice requires them: mutation testing, broad visual +regression, exhaustive Angular controller tests, `get-settings.php` coverage, +and RCVis cURL seams. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Two clients drift against one backend | Typed adapters, v2 contracts, MySQL contract tests, staged rollout | +| Authentication work expands the rewrite | Keep anonymous milestone first; require a separate auth design review | +| Existing shortcode links stop opening | Compatibility route, universal-link tests, web fallback | +| Election results differ after extraction | Run identical fixtures against legacy and TypeScript implementations | +| Native ranking is inaccessible | Provide buttons and screen-reader actions in addition to gestures | +| Offline retries create duplicate votes | No background queue; server remains authoritative; explicit retries | +| Expo web harms SEO or dynamic routes | Preserve Angular web until a deployment proof meets web exit criteria | +| Advanced browser features block parity | Classify each feature as universal, native-specific, or web-only | + +## First implementation PR + +The first PR should contain only: + +- `apps/mobile/` scaffold with TypeScript and Expo Router; +- environment-based API base URL configuration with a checked-in example; +- `LegacyApiClient.getBallot(key)` and runtime normalization; +- shortcode lookup and read-only ballot detail screens; +- loading, not-found, malformed-response, and network-error states; +- adapter unit tests and setup documentation; +- CI commands scoped to the Expo app. + +It should not include voting, authentication, database changes, production +deployment, universal-link association files, or modifications to legacy API +responses. That keeps the first review focused on repository layout, +connectivity, and the compatibility seam. + +## Open decisions + +These do not block the first PR: + +- Is Expo ultimately expected to replace the public website, or only provide + native apps? +- Which Apple bundle ID and Android package name will be used? +- Which staging domain will device builds use? +- Should the existing root-shortcode URL remain canonical permanently? +- Will legacy accounts migrate passwords gradually or require a reset? +- Which analytics and crash-reporting services are acceptable? +- Which advanced features are explicitly web-only? + +## Review gates + +Phase 0 was approved as the working direction on 2026-08-08. Revisit this RFC +after the read-only device spike, before production link association, and again +before Phase 3 authentication work.