Παράκαμψη στο περιεχόμενο
← Όλα τα Case Studies

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

3
Active staff
154
Client portal users
22/22
ETL entities
6.981 rows
17 → 7
Dashboard queries
RPC consolidation
10
SQL migrations
RLS + perf hardening
244 → 173
Lint errors
CI ratchet
564 lines
Mobile API spec
1634 lines
Runbook
Phase 0-6 migration + Phase 7 production hardening2026

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

Τι το κάνει διαφορετικό

01

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.

02

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.

03

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).

04

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.

05

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.

06

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 τα φυλάει.

07

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.

08

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.

Customer portal landing page σε mobile με dark theme

Mobile

Premium dark home, bridge σε iOS app + portal

Customer portal landing page desktop με glow effect

Desktop

Branded home με App Store CTA

Σελίδα σύνδεσης mobile με reCAPTCHA και App Store link

Mobile

Sign-in mobile με Cloudflare reCAPTCHA

Σελίδα σύνδεσης desktop με centered card layout

Desktop

Sign-in desktop με EN switcher και iOS deeplink

Tech Stack

Με τι χτίστηκε

Next.js 16Supabase self-hostedPostgreSQL RLSTypeScriptshadcn/uiCoolifyExpo SDK 55PHP compat layer

Όλο το 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 ώστε να μη ξανα-εμφανιστούν.

Παρόμοιο project σε εξέλιξη;

Αν χτίζεις κάτι παρόμοιο και ψάχνεις partner που να ξέρει το τεχνικό terrain, πες μας. 30λεπτη συζήτηση χωρίς δέσμευση.