Skip to content

fix(backend): serve the notifications the client already polls - #19

Merged
anilcancakir merged 3 commits into
mainfrom
fix/backend-notifications-feature
Sep 24, 2026
Merged

anilcancakir merged 3 commits into
mainfrom
fix/backend-notifications-feature

Conversation

@anilcancakir

Copy link
Copy Markdown
Member

What changed

The backend enables Features::notifications(), so api/v1/notifications and api/v1/notification-preferences exist. The notifications table is also keyed by UUID so the database channel can write to it.

Why

The Flutter client ships with notifications: true, so its notifications screen and bell poll these routes from the first sign-in, and every poll was a 404. Enabling the feature alone was not enough. This app runs on integer keys, and the published notifications migration gave the table an auto-incrementing id, while Laravel's database channel always writes a UUID there, so every notify() failed at insert. The package side is fluttersdk/magic-starter-laravel#44.

  • The published create migration uses $table->uuid('id')->primary() (fresh databases).
  • rekey_notifications_table_by_uuid is copied by hand from that PR's stub, because this app resolves 0.0.10, which does not ship it. It repairs a database that already migrated the integer table.
  • add_sms_registered_at_to_users_table, the third migration the installer publishes with the notifications feature, was never copied here; without it the first OneSignal SMS registration would fail on an unknown column.

Evidence

  • bin/check backend green (pint + PHP suite). New NotificationRoutesTest: before the change 4/4 routes answered 404, and test_a_database_notification_reaches_the_list failed with datatype mismatch once the feature was on and before the key fix.
  • Exercised for real:
    • against a copy of the real local dev database, migrate rebuilt the integer table with a UUID key and both indexes;
    • a real $user->notify() then answered GET /notifications (data.0.data.title), /unread-count ({"data":{"count":1}}), POST /{uuid}/read (200, count back to 0) and /notification-preferences;
    • in the Flutter web app (dusk, real Chrome) the notification rendered on /notifications at 1440 and 390 widths, and delete through the confirm dialog issued DELETE /notifications/{uuid} and emptied the table, with 0 exceptions.

Contract check

  • Depends on unreleased sibling code: the rekey migration is copied from magic-starter-laravel#44 and needs nothing newer than 0.0.10 (MigrationHelper::usesUuids() exists there).

Existing local databases: run php artisan migrate, since the rekey repairs the table in place.

The Flutter client ships with `notifications: true`, so its notifications
screen and bell poll `api/v1/notifications` from the first sign-in, while the
backend left `Features::notifications()` commented out and every poll was a
404.

Enabling the feature alone was not enough. This app runs on integer keys, and
the published notifications migration gave the table an auto-incrementing id,
while Laravel's database channel always writes the notification's own UUID
there: every `$user->notify()` failed at insert and the screen could only ever
be empty. The table's key is now a UUID in both modes; the morph columns still
follow `use_uuids`. The same fix goes into magic-starter-laravel's stub.

An existing local database keeps the old table; `php artisan migrate:fresh`
rebuilds it. Nothing could have written to it through the channel.
The fixed create migration only reaches a fresh database; its hasTable guard
leaves a table an earlier migrate built with an auto-incrementing id, which
the database channel still cannot write to. The rekey migration, copied by
hand from magic-starter-laravel's unreleased stub (this app resolves 0.0.10,
which does not ship it), rebuilds that table with a UUID key and carries any
rows over; on a correct table it does nothing, and a run that stops part-way
can be re-run. Against a copy of a real local database it turned the integer
table into the UUID one with both indexes, and a notify() then listed over
the API.

add_sms_registered_at_to_users_table is the third migration the installer
publishes with the notifications feature; it was never copied here, so the
first OneSignal SMS registration would have failed on an unknown column.
@kodizm

kodizm Bot commented Sep 24, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Looks correct: I found no defects that should block the merge. The one gap is that no test runs the new rekey migration's rebuild path.

I read all five changed files. config/magic-starter.php already imports FlutterSdk\MagicStarter\Features, so the bare Features::notifications() resolves.

The create stub now always writes uuid('id')->primary(), and that matches what Laravel's database channel inserts.

In 2026_09_24_000020_rekey_notifications_table_by_uuid.php, the rebuild only runs when id is auto-increment. It refuses to run when the table has any column outside the eight it copies. It carries read_at and the timestamps over. It can be re-run after a partial failure. The indexes are created after the rename, so their names match the ones the stub creates.

It assigns new ids to existing rows on purpose. That is safe because, as its comment says, those ids never came from the channel. The empty down() is documented, and I agree with the reason given.

Minor

backend/database/migrations/2026_09_24_000020_rekey_notifications_table_by_uuid.php:62: (maintainability) Nothing in CI runs the rebuild (copy, drop, rename, pgsql primary-key rename). Under RefreshDatabase the stub already builds a UUID table, so this migration only runs ensureIndexes(). It was checked by hand against a local database. Nobody works on it again after merge, and a copied migration that can drop a table is worth a regression test: create the old integer-keyed table, insert a row, run up(), and assert the row survives under a UUID key.

Tests

tests/Feature/NotificationRoutesTest.php covers the four routes, the guest 401 and a real database-channel notify() on a fresh schema. Nothing covers the in-place rekey of an existing table, or the new sms_registered_at column.

CI

  • Backend (pint + tests): success
  • Flutter (analyze + test): success
  • Published graph (hosted resolution): success
  • Design tokens: success
  • Instruction mirrors: success
  • Auto-merge low-risk Dependabot PRs: skipped

The rekey is a hand-copied stub that drops and rebuilds a table, and under
RefreshDatabase the create migration already builds a UUID table, so CI never
ran its rebuild path. The test puts back the integer-keyed shape, inserts a
row, runs up(), and checks the row survives under a UUID with both indexes
and that the database channel writes again; forcing the auto-increment check
to false turns it red. A second test pins the sms_registered_at column.

DatabaseOnlyNotification moves to its own file, since a second test now uses
it and PSR-4 cannot find a class declared inside another test's file.
@anilcancakir
anilcancakir merged commit 0018e04 into main Sep 24, 2026
7 checks passed
@anilcancakir
anilcancakir deleted the fix/backend-notifications-feature branch September 24, 2026 11:02
@kodizm

kodizm Bot commented Sep 24, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Looks correct: the new commits fix the test gap from my last review and add no defects.

What changed since my last review: the commits after df6c897 touch test files only.

  • DatabaseOnlyNotification now has its own file, so the new test can load it through the Tests\ namespace.
  • RekeyNotificationsMigrationTest is new.

The new rekey test:

  • It rebuilds the old integer-keyed table (id(), morphs() and the composite index), inserts a row with id 7, then requires the migration and runs up().
  • It asserts that the row survives under a UUID with its data unchanged, and that both index names match the create stub's.
  • It then checks that a real database-channel notify() can write to the rebuilt table.

The test passes, and the Str::isUuid assertion only holds if keyedByAutoIncrement() detected the old key on SQLite. So the test does exercise the drop and rename path, not only ensureIndexes(). That settles my earlier Minor finding.

The new column test: a second test asserts that users.sms_registered_at exists. That covers the other gap I noted.

The one path CI still does not run is the PostgreSQL primary-key rename in promoteScratchTable(), because the suite uses in-memory SQLite (phpunit.xml). That is a limit of the test setup, not a defect in this change.

Tests

RekeyNotificationsMigrationTest covers the in-place rebuild on SQLite and the sms_registered_at column. NotificationRoutesTest still covers the routes.

CI

  • Backend (pint + tests): success
  • Flutter (analyze + test): success
  • Published graph (hosted resolution): success
  • Design tokens: success
  • Instruction mirrors: success
  • Auto-merge low-risk Dependabot PRs: skipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant