⚠️ This article is a translation from French.
I'm writing first in French and then translate it to English, so it can contain some inaccuracies.
The translation is made by hand and no translation tools are used.
For the original version, please refer to the French version.
NTFY is a service to notify users at any time for just about anything as long as an HTTP request can be made.
I really like this project which I personally use, but I didn’t like the web application, nor the IOS application because of some limitations.
So I decided to create a project under sveltekit to challenge myself a little and to bring some additional features.
What is NTFY ?
Ntfy is an open-source tool that allows you to easily send notifications via an HTTP call.
You can easily subscribe to these messages to receive notifications.
The ntfy.sh website allows direct use of the service or it can also be self-hosted.
The client that receives the notifications can of course also be desktop via the web UI or via the Android and IOS apps
Why make an additional web client?
There is already a web client, however the PWA installation is not complete on IOS, in fact we cannot have notifications.
Also, the UI is material-based and not to my taste.
I used NTFY’s iOS app, but it’s limited. It’s impossible to display messages in Markdown and only allows you to receive messages.
MVP
The MVP defined here was mainly to have a more modern and simple UI as well as to have notifications in PWA and reading of markdown.
Challenges of this project
The biggest challenges were using sveltekit and being able to subscribe to the JSON feeds that NTFY offers to receive new messages.
The technical stack
Sveltekit
Sveltekit was the first choice for this project because having already had experience with it, I was on familiar ground.
Remote functions
But recently, Sveltekit launched remote functions, and I really wanted to give them a try.
The remotes functions are functions called from the client side and will directly interact with the back-end.
In a code without remote functions it would be necessary:
- Create the use case (optional, depending on your architecture)
- Create a backend API route
- Create the fetch call in the client side
- Handle normalization, serialization…
With the remote functions, we reduce the steps to 3 :
- Create the use case (optional, depending on your architecture)
- Create the remote function
- Call the remote function on the client side
This removes a lot of code from having to manage API calls, etc.
Example to list some topics
One file data.remote.ts (the .remote.ts extension is specific for the Remote functions)
import { query } from '$app/server';
import { listAllTopics } from '@/server/repository/TopicsRepository';
export const getTopics = query(async () => {
return await listAllTopics();
});
In the previous snippet, we fetch directly the topics from the repository, then we call this getTopics function inside a svelte component.
<script lang="ts">
import { getTopics } from '../../../routes/topics/data.remote';
let topics = getTopics();
</script>
In the previous snippet, the topics variable is holding all the topics.
Drizzle and SQLite
As a Prisma regular, I wanted to try Drizzle, which has been gaining popularity recently.
So, a new ORM means relearning the logic. Where Prisma uses document-oriented queries, Drizzle uses the fundamentals of SQL.
So, we’re back to using SQL, but with functions (similar to Doctrine in PHP).
Example of a query to retrieve topics
return await db
.select({
id: topic.id,
name: topic.name,
newCount: sql<number>`COUNT(CASE WHEN ${message.id} IS NOT NULL AND ${message.viewed_at} IS NULL THEN 1 END)`,
notificationActive: topic.notificationActive
})
.from(topic)
.leftJoin(message, eq(topic.id, message.topicId))
.groupBy(topic.id)
.orderBy(topic.name);
Technical concerns
Docker
I’m used to using Docker in my projects to launch databases or services I need, but I’ve never created my own images with the hope of sharing them with everyone.
This is one of the tasks that took me the most time: how to generate an image that will perform specific tasks (e.g., database migrations) upon launch.
Synchronization
The advantage of the NTFY service is that it can notify users very quickly (even instantly). To do this, see the NTFY documentation for a JSON stream that can be subscribed to per topic in order to receive new notifications as quickly as possible.
To achieve this in SvelteKit, I tested the solution of loading topic subscriptions into hooks.server.ts. I don’t yet know if this solution is viable in the long term, so I might update it if the method changes.
So, in terms of usage, each time a user adds a topic, we subscribe to the changes, and if the topic is deleted, we remove the changes from the stream.
Notification
Notifications are sent via the Notification API.
Whenever a user arrives and notifications are not enabled, we prompt them (in a non-intrusive manner) to enable them. When we enable them, if the request is successful, we install the service worker that will run in the background.
Then, on the front end, we retrieve their subscription, which we save in the database so that we can send them notifications directly in the background.
SSE
In order to access new messages, I used the SSE to refresh the view on the sidenav of the number of messages, but it would also be necessary to refresh the list of notifications.
Roadmap
- Being able to send notification (pour le moment c’est juste de la souscription)
- Possibility to set the value of priority where we trigger notification
- Being able to configure all the devices register to allow or not notifications