Making a Desktop app with Elm, Deno & Velociraptor (and now Tauri)

Hello All!

I’d like to describe the setup for an Elm desktop app that I have been experimenting with. It gives the app list, read, and write access to designated directory on the user’s computer. Authors like to have ownership of their work!

The current solution is quite clunky, so I’d very much like some feedback on whether (a) this is a feasible approach, (b) how to make it more robust, and [c] how to improve it (a totally different approach is OK).

The app in question bundles a pure Elm text editor with compilers to Html for an extended version of Markdown and MiniLaTeX, which is a subset of LaTeX. The extended version of Markdown permits one to render LaTeX-style formulas, SVG images, and poetry. See Extended Markdown Live to see how this flavor of Markdown works. Also, see screenshots of the app at the end.

The command-line script for the app uses Velociraptor, and it is invoked by saying vr app. Velociraptor runs shell scripts as well as Deno scripts and programs. Deno is a successor to Node which is written by Ryan Dahl, the developer of Node. Running vr app does two things. The first is to launch the Elm app using the Deno program file_server. The second is to launch a web server written in Deno. Such a server is written in Typescript and makes use of the Deno standard library. The Elm app then talks to the server via Http. As a result, the Elm app can list files in the designated director, read them into memory, and write them back to disk as they are are edited. The same setup can be use to publish the files on the web via the same server program hosted remotely.

Installation, part 1

Now comes the sticky points. How can one install and run such an app?

The very poor solution I have at the moment is to extract the needed files and directories from the development project and store them in a directory, say app. To deploy the app, one must copy this directory to one’s computer. (It could be bundled into a tar file, for example). Here is the contents of that directory:

.
├── Main.js
├── assets
├── data
├── index.html
├── scripts.yaml
└── server

3 directories, 3 files

You see some familiar characters: Main.js is the compiled Elm app which is referenced in index.html. The index.html file imports various resources from assets, e.g, MathJax.js, which is used to render mathematical formulas. The data directory is a default directory for the user’s files. Finally, server contains the Typescript files for the web server.

Installation, part 2

Once the directory has been copied/extracted, one has to set up Deno and Velociraptor, e.g.,

$ curl -fsSL https://deno.land/x/install/install.sh | sh
$ deno install -qA -n vr https://deno.land/x/velociraptor/cli.ts

Running the app

Then, to run the app, cd into app and say vr app. When one does this, vr finds the first scripts.yaml in the current directory of a parent, then runs that script with the given argument. Here is the full script:

env:
  APP_ROOT: /Users/jxxcarlson/dev/elm/projects/muEdit
  DATA_ROOT: /Users/jxxcarlson/Documents/mudocs
scripts:
  app:
    desc: run editor-app & start the server
    cmd:
      pll:
        - file_server ${APP_ROOT}
        - cmd: ${APP_ROOT}/server/server.ts
          allow:
            - net
            - env
            - write
            - read
        - open -a /Applications/Google\ Chrome.app/  http://0.0.0.0:4507/index.html

Note that the user must configure APP_ROOT, which is where the files Main.js etc. live, as well as DATA_ROOT, which is where the user’s files will be stored. The commands listed under pll will be executed in parallel. The allow clause gives Deno permission to access to the file system, the net, and the environment.

Robustness

Accepting for the moment the extreme hackiness of this solution, there are at least two serious problems:

  • Latency. The server is recompiled each time one invokes vr app. That takes some small number of seconds. As a result, one may see an empty file list if one lists the files too quickly. This can be solved by precompiling the server, but I haven’t done that yet.

  • Shutdown. It can happen that the processes for file_server and/or server are still running when the user invokes vr app at a future date. Then the user will get an error. Of course you can use ps aux | grep WHATEVER and kill -9 to fix things, but who wants to do that?

One can use vr export app to compile a version of the needed script invocation, then copy it from ./bin to ~/.deno/bin/ so that the the app can be invoked from anywhere by saying app. Of course one has to put ~/.deno/bin/ in ones PATH. A small but useful improvement.

Conclusions and self-criticisms

Ugh! This approach works, but installation requires many steps. Too much!! And too much user configuration. While this approach does give a way for Elm apps to have list, read, and write access to the file system, it is an exercise in extreme hackery, useful for at most one person in the universe.

There must be a better way. One approach, which I have not studied yet, but which may be the way to go, is embodied in Aaron vonderHaar’s elm-desktop-app. As I understand it, it accesses the user’s file system using ports to persist state, and so could likely be adapted to the use case I envisage. What is needed is the ability to read and write files in in designated directory and also the ability to read, parse, and write a file manifest.yaml which lists file names and other metadata. No Json here, because one design goal is for everything to be (easily) human-readable.

Anyway, I look forward to your suggestions, both for improving the current approach and to moving to a better one. The ultimate goal is for a simple one-step installation process that non-developers can use (studens, teachers, math & science professors, etc.)

As a side note, I would like to say that I have very much enjoyed working with Deno and Velociraptor. Clean and elegant! (Here is a blog post on Deno).

Screenshots

8 Likes

On a similar-ish note I had developed CAEFTE: https://github.com/miniBill/caefte.
This uses C# as the webserver, and the user normal browser as the UI container.

Advantages: tiny, TINY self-contained executables.
Disadvantages: requires mono on Linux and macOS, the application opening a browser tab for interaction is something very different from what an user normally expects. [this could be improved by using CEF or a modern WebView ]

It could be interesting to investigate using rust [or even go!] for the webserver, this should produce small-ish self-contained applications.

Deno looks interesting, except for one thing: importing code directly from an URL is something that looks like a huge, HUGE security risk. Am I missing something obvious?

2 Likes

Re security, that is an excellent point. I did a little investigating, and found that one can restrict Deno’s read/write access to a specified directory in the .yaml file:

  server:
    cmd: ./server.ts
    allow:
      - net
      - env
      - write=${DATA_ROOT}
      - read=${DATA_ROOT}

There may be other holes, but this plugs a big one,

1 Like

You’re still giving access to that directory to anyone who controls any webserver you’re importing from and all their dependencies. I find it quite scary tbh

1 Like

Regarding security, it was pointed out either on Hacker News or Lobsters that all dependencies you include have access to the same security allowances. I.e. if you provide net access because you’re writing a server, then that module you imported for text formatting also has net access even if it doesn’t need it.

2 Likes

I did look into using Deno for something like CAEFTE, because I remember trying and liking the idea. Last I saw there was still an open GitHub issue for being able to build Deno into an exe or similar, which is what I’d want ideally.

1 Like

I see that my current approach is seriously flawed. Will have to rethink. Any recommendations on a secure local server?

Did take a look at the open Github issue that @wolfadex mentions above. If you scroll down towards the bottom, you will see some interesting comments by Wallacy 29 days ago.

Related question. I could possibly try using ports to talk to JS and then use the fs library to talk to the file system, eliminating the local server. But to do this, I have to say const fs = require('fs'); — which JS does not like. This Node syntax. Any ideas on how to proceed?

I’ll look at CAEFTE … not familiar with C#, but maybe this could work for me. How does it solve the security issue?

1 Like

There is a HTML5 File-API, even having Elm-bindungs, but due to security limits user interaction is required. It looks and feels to the user more as upload and download than loading and saving. So Electron or a similar architecture embedding the browser/webview is necessary to have a native look and feel. An other example is in https://github.com/huytd/kanban-app , but I have not tried yet.

Thanks! I will take a look at that.

Just an other strange idea: Store the data in the browser and offer to “backup” it.
But so far I have no real trust, that the users or the browser do not delete it.

I had implemented an option to keep the data in local storage, but disabled it in favor of th e above solution – too easy to lose data. It is looking more and more that Electron is the way to go.

Wrt security: it depends on your threat model. If you fear someone able to connect to localhost ports abusing the server I guess the simplest thing would be to have the server listen on http://localhost:port/SomeRandomBase64String/.
The security issue I was lamenting wrt Deno is importing code from URLs without any kind of check. In C# you usually import a few big packages from well-known sources, sidestepping most of the problems. But this is a naive high-level view of it, again, without a threat model there is no security.

Electron is a decent solution for this kind of applications. Main advantages: stable, secure, native interop without having a server, tighter integration with native GUIs, huge community and libraries for everything.

Caefte is beta software, but it does have a couple advantages: tiny executables [we’re talking a couple order of magnitude difference wrt node], native interop is in C# [which I personally prefer over JS, YMMV], consumes waaaay less RAM, expecially when the GUI is “closed”, works with the user normal browser. Disadvantages are: not stable, security is not 100% there [depends on theat model], you have a server listening on an open port [on localhost though, and you could add auth], closing the last tab doesn’t “close” the application [this could be done though! possibly with websockets?]

Kinda curious if it’d be easier to use something like https://github.com/Boscop/web-view? They even recommend pairing it with Elm or PureScript

1 Like

Have you considered PWA?

Tauri bills itself as superior to Electron.

Framework agnostic toolchain for building highly secure native apps that have tiny binaries and are very fast.

1 Like

Thanks for the suggestions and info. One thing that may help is that I’ve implemented authorization for the server: the user has to have created an account & then sign in to access files in the designated directory. Do you think that this adequately addresses the security issue?

I will probably pursue the Deno server approach to see how far I can get with it, then think about what really should be done.

Very interesting! I like the idea of lean, mean, and small.

I will look into this. Thanks for the info.

Great! Will check this one out too. So good to see that there a many alternatives.

Hi @akoppela! What is PWA?