Internal CRM / Wideview
Wideview Support
Migration από legacy CRM σε Next.js + Supabase με zero-update mobile compat
10 SQL migrations, RLS-hardened, mobile API spec 564 lines, runbook 1634 lines
Challenge
Τι έπρεπε να λυθεί
Το παλιό CRM είχε φτάσει το end of useful life: αργό admin, παλιό UI, δύσκολο API extension, custom modules σπάγαν σε κάθε update. Παράλληλα, υπήρχε App Store mobile app (Expo, ~4 active devices) που χτυπούσε `wideview_api.php` με static JWT, 154 client portal users + 4 mobile push tokens, και 156 contacts με bcrypt $2a$08$ hashes που έπρεπε να μεταφερθούν χωρίς να ξαναμπούν credentials. Cutover χωρίς downtime, χωρίς να σπάσει το mobile.
Solution
Τι χτίσαμε
Strangler fig migration: νέο Next.js 16 + self-hosted Supabase σε s3 Coolify, με PHP compat layer στο `/wideview_api.php` που mimics το παλιό endpoint contract bit-for-bit. Bcrypt password_hash imported direct σε Supabase Auth → zero password resets. Brand-variant model (Wideview default + social24 + dosmart + Wide Music Records) αντί για multi-tenant. Full ETL 22/22 entities (6.981 rows), 5-phase production hardening (10 SQL migrations, RLS audit, mobile contract tests, restore drill, lint ratchet 244→173 με CI gate).
Custom Modules
Τι το κάνει διαφορετικό
PHP compat layer για mobile app continuity
`/wideview_api.php` route στο Next.js mimics το παλιό endpoint contract (60+ actions). Mobile app App Store v5.3.0 build 5 (Expo SDK 55, RN 0.83) συνεχίζει να καλεί τα ίδια URLs με τα ίδια JSON shapes. Zero forced app update. Detailed mobile API spec doc 564 lines, version-locked με do-not-rename column list, 14 fragility points από Phase 4 audit.
Bcrypt password import + brand variant model
Read-only ETL από MySQL → Supabase Auth μέσω `password_hash` field. 9 staff + 156 contacts μπήκαν χωρίς να αλλάξουν τον κωδικό τους. Brand variants (όχι multi-tenant): wideview (default Wideview Entertainment), social24, dosmart, widemusic. Κάθε brand έχει logo (light/dark/circular), accent + secondary color, tagline, default reply-to email.
Full ETL 22/22 entities (6.981 rows)
8 idempotent migration scripts με `legacy_perfex_id ON CONFLICT`. ETL infrastructure (`scripts/dump-perfex.sh` + lib): SSH-based MySQL reader με `JSON_ARRAYAGG + JSON_OBJECT` για newline/quote-safe transport, και `SET SESSION group_concat_max_len = 268435456` για να μη χτυπάει 1MB cap. Tickets(87) + replies(520), invoices(34) + estimates(10) + proposals(152) + payments(36) + line_items(588), projects(160) + tasks(422) + comments(2.097) + assignees(525) + checklist(468), contracts(3), files(1.102 metadata), notes(16), tags(4).
RLS hardening + 10 SQL migrations
5-phase audit έδειξε ότι `platform_settings` + `push_hooks` είχαν `WRITE: true/true` (any authenticated user incl. portal contacts μπορούσε να γράψει). Migrations έκλεισαν, με dedicated RLS test suite (`supabase/tests/rls-policies.test.sql`) που rejects `USING true / WITH CHECK true` σε writes. clients_portal_update_policy fix για το silent UPDATE fail στο portal company form.
client_detail_summary RPC: 17→7 queries
`/admin/clients/[id]` έκανε fan-out 11 queries `count: 'exact', head: true` + invoice aggregate. Νέο `client_detail_summary(uuid)` RPC επιστρέφει 11 counts + invoice aggregate σε ένα round-trip. UrlTabs άλλαξε σε `window.history.replaceState()` αντί `router.replace()` για να μη γίνεται SSR re-render σε κάθε tab click.
5 silent mobile bugs caught + fixed
(1) `files.storage_bucket` column missing → mobile uploads/downloads silently failing. (2) `filesize` vs `filesize_bytes` → PostgREST silently dropped value, every file landed με NULL size. (3) `permission_id` vs `legacy_perfex_permission_id` → mobile menu permissions πάντα []. (4) `proposals` polymorphic mismatch (rel_type/rel_id αντί client_id) → 0 proposals σε mobile client_detail. (5) storage_bucket selection σε download. Όλα fixed, mobile contract test suite τα φυλάει.
1.634-line runbook + restore drill
Operations runbook με 10 incident scenarios + 6 routine ops, copy-paste recovery procedures, contact tree. Restore drill: σκόπιμη απώλεια test client σε staging, restore σε 18 λεπτά από Supabase point-in-time backup. Cleanup cron σβήνει stripe_processed_events (90d), api_idempotency (7d), api_rate_limits (24h) με explicit conditions (όχι blanket-delete). Daily backup `/var/backups/wideview/db-YYYY-MM-DD.sql.gz` σε s3 + off-site mirror σε s2.
Coolify deployment + lint ratchet CI gate
Repo `tsokasg89/wideview-support`, Coolify auto-deploy σε push, app uuid `mt4etssavy5k6ajje3041vhd`, Supabase service uuid `zo2242rccnmqgksf72ngredx`. Source mirror στο s3 `/root/wvs-sync` για quick interventions. CI workflow με `MAX_DIAGNOSTICS=173` threshold (down από 244): PR δεν περνά αν errors ανέβουν, σταδιακό cleanup χωρίς να μπλοκάρει νέο work.
Live από production
Πώς δείχνει στην πράξη
Screenshots από το ζωντανό site. Τιμές, ονόματα πελατών και ευαίσθητα στοιχεία είναι μασκαρισμένα με skeleton blur ώστε να φαίνεται μόνο το functionality.

Mobile
Premium dark home, bridge σε iOS app + portal

Desktop
Branded home με App Store CTA

Mobile
Sign-in mobile με Cloudflare reCAPTCHA

Desktop
Sign-in desktop με EN switcher και iOS deeplink
Tech Stack
Με τι χτίστηκε
Όλο το stack είναι τυποποιημένο. Δεν βασίζεται σε κρυφά παραμετροποιημένα plugins ή proprietary cloud services. Μπορεί να μεταφερθεί ή να συντηρηθεί από οποιαδήποτε ομάδα γνωρίζει το stack.
FAQ
Συχνές ερωτήσεις
Έπρεπε να ξανα-εισάγουν οι 154 πελάτες τα passwords τους μετά τη μετάβαση;
Όχι. Έγινε read-only ETL από MySQL → Supabase Auth μέσω password_hash field, που δέχεται bcrypt $2a$08$ hashes ως είναι. 9 staff + 156 contacts μπήκαν χωρίς να αλλάξουν τον κωδικό τους και χωρίς να σταλεί reset email. Στην πρώτη επόμενη login λειτούργησαν κανονικά με τους ίδιους κωδικούς που χρησιμοποιούσαν στο παλιό CRM.
Συνέχισε να δουλεύει το App Store mobile app μετά το cutover;
Ναι, zero forced app update. Στήσαμε PHP compat layer στο /wideview_api.php route στο Next.js που mimics το παλιό endpoint contract bit-for-bit (60+ actions). Mobile app App Store v5.3.0 build 5 (Expo SDK 55, RN 0.83) συνεχίζει να καλεί τα ίδια URLs με τα ίδια JSON shapes. Έχουμε γράψει mobile API spec doc 564 lines που τεκμηριώνει version-locked column names και 14 fragility points για να μη σπάσει σιωπηλά σε κάποιο μελλοντικό refactor.
Χάθηκαν στοιχεία πελατών στη μετάβαση;
Όχι. Full ETL 22/22 entities, 6.981 rows total. Tickets (87) + replies (520), invoices (34) + estimates (10) + proposals (152) + payments (36) + line_items (588), projects (160) + tasks (422) + comments (2.097) + assignees (525) + checklist (468), contracts (3), files (1.102 metadata), notes (16), tags (4). Τα ETL scripts είναι idempotent με legacy_perfex_id ON CONFLICT, οπότε re-run δε δημιουργεί duplicates. Παλιό CRM παραμένει read-only στο old.wideview.support για άμεση αναφορά αν χρειαστεί.
Πώς λύθηκε η ασφάλεια του παλιού CRM που είχε permissive RLS;
5-phase audit έδειξε ότι platform_settings + push_hooks είχαν WRITE: true/true (any authenticated user incl. portal contacts μπορούσε να γράψει). Στήθηκαν 10 SQL migrations που τα έκλεισαν, με dedicated RLS test suite (supabase/tests/rls-policies.test.sql) που rejects USING true / WITH CHECK true σε writes. Επίσης clients_portal_update_policy fix για το silent UPDATE fail στο portal company form, που έσπαγε το client portal χωρίς error message.
Πόσο γρήγορο είναι το client detail page τώρα;
Το /admin/clients/[id] έκανε fan-out 11 queries (count: exact, head: true) + invoice aggregate. Νέο client_detail_summary(uuid) RPC επιστρέφει 11 counts + invoice aggregate σε ένα round-trip, οπότε από 17 queries πήγαμε σε 7. UrlTabs άλλαξε σε window.history.replaceState() αντί για router.replace() ώστε να μη γίνεται SSR re-render σε κάθε tab click. Το page load αρκετά πιο γρήγορο σε production.
Έχει disaster recovery plan για το νέο CRM;
Ναι. Operations runbook 1.634 γραμμές με 10 incident scenarios + 6 routine ops, copy-paste recovery procedures, contact tree. Έγινε επίσης restore drill: σκόπιμη απώλεια test client σε staging, restore σε 18 λεπτά από Supabase point-in-time backup. Daily backup /var/backups/wideview/db-YYYY-MM-DD.sql.gz σε s3 + off-site mirror σε s2. Cleanup cron σβήνει stripe_processed_events (90 ημέρες), api_idempotency (7 ημέρες), api_rate_limits (24 ώρες) με explicit conditions, ποτέ blanket-delete by date.
Πώς αποφεύγονται σιωπηλά bugs σε mobile;
Φτιάξαμε mobile contract test suite που πιάνει column name mismatches. Κατά τη μετάβαση εντοπίσαμε 5 silent mobile bugs: (1) files.storage_bucket column missing → uploads/downloads silently failing, (2) filesize vs filesize_bytes → PostgREST silently dropped value, (3) permission_id vs legacy_perfex_permission_id → mobile menu permissions πάντα άδεια, (4) proposals polymorphic mismatch → 0 proposals σε mobile client_detail, (5) storage_bucket selection σε download. Όλα fixed, και η contract test suite τρέχει σε CI ώστε να μη ξανα-εμφανιστούν.