OK here is what I know.
Here is how the notifications table is structured:
mysql> show columns from prefix_xoopsnotifications;
+--------------+-----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-----------------------+------+-----+---------+----------------+
| not_id | mediumint(8) unsigned | | PRI | NULL | auto_increment |
| not_modid | smallint(5) unsigned | | MUL | 0 | |
| not_itemid | mediumint(8) unsigned | | MUL | 0 | |
| not_category | varchar(30) | | MUL | | |
| not_event | varchar(30) | | MUL | | |
| not_uid | mediumint(8) unsigned | | MUL | 0 | |
| not_mode | tinyint(1) | | | 0 | |
+--------------+-----------------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
And here is an example if its content:
mysql> select * from prefix_xoopsnotifications;
+--------+-----------+------------+---------------+------------------+---------+----------+
| not_id | not_modid | not_itemid | not_category | not_event | not_uid | not_mode |
+--------+-----------+------------+---------------+------------------+---------+----------+
| 1 | 2 | 1 | category_item | bookmark | 2 | 0 |
| 2 | 2 | 0 | global_item | published | 150 | 0 |
| 6 | 2 | 0 | global_item | category_created | 3 | 0 |
| 5 | 4 | 0 | global | new_fullpost | 1 | 0 |
| 7 | 2 | 0 | global_item | published | 3 | 0 |
| 8 | 4 | 1 | forum | new_thread | 3 | 0 |
| 9 | 4 | 1 | forum | new_post | 3 | 0 |
| 10 | 2 | 0 | global_item | published | 153 | 0 |
| 11 | 4 | 0 | global | new_post | 12 | 0 |
| 12 | 2 | 0 | global_item | published | 12 | 0 |
| 13 | 2 | 0 | global_item | published | 11 | 0 |
| 14 | 4 | 0 | global | new_post | 11 | 0 |
| 15 | 2 | 0 | global_item | published | 8 | 0 |
| 16 | 4 | 0 | global | new_post | 8 | 0 |
| 17 | 2 | 0 | global_item | published | 13 | 0 |
| 18 | 4 | 0 | global | new_post | 13 | 0 |
| 19 | 2 | 0 | global_item | published | 14 | 0 |
| 20 | 4 | 0 | global | new_post | 14 | 0 |
| 21 | 2 | 0 | global_item | published | 16 | 0 |
| 22 | 4 | 0 | global | new_post | 16 | 0 |
| 23 | 2 | 0 | global_item | published | 17 | 0 |
| 24 | 4 | 0 | global | new_post | 17 | 0 |
| 25 | 2 | 0 | global_item | published | 18 | 0 |
| 26 | 4 | 0 | global | new_post | 18 | 0 |
| 27 | 2 | 0 | global_item | published | 19 | 0 |
| 28 | 4 | 0 | global | new_post | 19 | 0 |
| 29 | 2 | 0 | global_item | published | 20 | 0 |
| 30 | 4 | 0 | global | new_post | 20 | 0 |
| 31 | 2 | 0 | global_item | published | 21 | 0 |
| 32 | 4 | 0 | global | new_post | 21 | 0 |
| 33 | 2 | 0 | global_item | published | 22 | 0 |
| 34 | 4 | 0 | global | new_post | 22 | 0 |
| 35 | 2 | 0 | global_item | published | 23 | 0 |
| 36 | 4 | 0 | global | new_post | 23 | 0 |
| 37 | 2 | 0 | global_item | published | 24 | 0 |
| 38 | 4 | 0 | global | new_post | 24 | 0 |
| 39 | 2 | 0 | global_item | published | 218 | 0 |
| 40 | 4 | 0 | global | new_post | 218 | 0 |
| 41 | 2 | 0 | global_item | published | 25 | 0 |
| 42 | 4 | 0 | global | new_post | 25 | 0 |
| 43 | 2 | 0 | global_item | published | 173 | 0 |
| 44 | 4 | 0 | global | new_post | 173 | 0 |
| 45 | 2 | 0 | global_item | published | 68 | 0 |
| 46 | 4 | 0 | global | new_post | 68 | 0 |
| 48 | 2 | 0 | global_item | published | 27 | 0 |
| 49 | 4 | 0 | global | new_post | 27 | 0 |
| 50 | 2 | 0 | global_item | published | 39 | 0 |
| 51 | 4 | 0 | global | new_post | 39 | 0 |
| 54 | 4 | 113 | thread | new_post | 3 | 0 |
+--------+-----------+------------+---------------+------------------+---------+----------+
49 rows in set (0.00 sec)
not_id is the notification identity number. When a notification is cancelled, XOOPS deletes the relevant row (and doesn't change any of the other not_id row numbers). New notifications are given the next highest not_id number (auto_increment) rather than filling in any gaps.
not_modid identifies the relevant module. On my site 2 is smartsection and 4 is cbb.
not_itemid is module-specific and identifies the relevant item. Most here are 0 as they refer to the module generally, but note not_id 54 where the not_itemid is 113 (the thread number to be notified about).
not_category and not_event explain themselves and are module-specific again.
not_uid identifies the relevant user by his user number.
not_mode - well, I honestly don't know what it means, but stick with 0 and you can't go wrong!
For 20 users you could easily use MySQL commands to add 20 rows for 20 new notifications. For more users I suppose you could write a PHP script to do it automatically. If you do that then submit it here! It could make a good module.
To delete users who haven't logged in for a while you'd just need to delete (or not) the relevant prefix_xoopsnotifications rows depending the value of the last_login column of the prefix_users table. I haven't time now to think it through fully.
I don't think that the table is referred to elsewhere so don't think there would be any problems with adding to it via MySQL (back it all up first though!) I've deleted rows with no problems but haven't had to add any yet.
Good luck!