# journiv's quick start ships a placeholder secret key and omits the media worker

> A self-hosted private journal served from a FastAPI origin that carries two compiled frontends, one of them a legacy mobile app kept temporarily. The single-command install passes a literal placeholder secret key, deliberately opts into insecure cookie auth, and starts no task worker, so uploaded images and video stay stuck in a processing state.

**journiv/journiv-app** — Journiv - Self hosted private journaling app

- Repository: https://github.com/journiv/journiv-app
- Website: https://www.journiv.com
- Stars: 1,221 · Forks: 51
- Language: JavaScript
- License: NOASSERTION
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/journiv-journiv-app

## The one command install passes a literal placeholder secret key

The quick start is a single container run with six environment variables, a named volume and a published port:

```bash
docker run -d \
  --name journiv \
  -p 8000:8000 \
  -e SECRET_KEY=your-secret-key-here \
  -e DOMAIN_NAME=192.168.1.1 \
  -e ALLOW_INSECURE_COOKIE_AUTH_OVER_HTTP=true \
  -v journiv_data:/data \
  --restart unless-stopped \
  swalabtech/journiv-app:latest
```

One of those variables is the application's secret key, and its value is the placeholder string naming a secret key.

That is the value in the command. Anyone who copies the line gets it, and the key is what signs sessions and tokens for a journal application.

The production compose file in the same repository has the right instruction. Its header lists the required variables, and for the secret key it gives a one-line command that prints a URL-safe random token. So the correct value is documented three directories away from the shortcut that does not use it.

The same command sets a domain name to a private LAN address and opts into insecure cookie auth over plain HTTP, with a warning directly beneath it that HTTP exposes passwords and refresh cookies to interception by other devices on the network, and that the HTTPS scheme setting should be used for any internet-accessible or untrusted deployment. The port is published as 8000 on all interfaces, not bound to the LAN address.

The production compose file inverts that default. It sets the same variable to false by default with a comment saying production stays fail-closed unless a trusted LAN operator explicitly opts in. So the two documented paths disagree on the secure default, and the one aimed at first-time users is the permissive one.

## The one command install leaves every media file in a processing state

The feature list advertises media uploads. The quick start cannot complete one.

A note above the command explains that it starts a minimal version of the application. Media files can be uploaded, but images, video and audio will stay in a processing state and will not appear in entries, unless a message broker and a task worker are present. Import and export jobs need the worker too, and the note points at the complete compose file for those.

So the first-run experience of the headline feature is a spinner that never resolves, with no error message, on the command the page presents as the quick way to try the application.

The missing piece is not small. The dependency list includes a task queue with a Redis-backed scheduler, a Redis client, a streaming JSON parser, an image library, a HEIF decoder, a file type sniffing library, and a media wrapper, and the compose file runs the application image twice, once as the web application and once as the worker, with both gated on the database and the broker reporting healthy.

There are nine compose files at the repository root, including SQLite variants, a development variant, a continuous-integration override, an override for older versions, and three that add a sidecar. The complete file is one of nine, and the quick start deliberately does not use it.

## Two frontends in one image and a service worker built to delete itself

One FastAPI origin serves everything, and the routing table has six entries.

The root path serves a React frontend, described as primary and as the default from this release onward. A legacy path serves a Flutter frontend, described as a temporary legacy interface that remains available for users who need to switch back during the transition and may be removed in a future release. The rest are the versioned API, public publishing routes, the framework's own documentation when enabled, and the schema document.

So the image carries two compiled frontends, built into two paths under the install directory with separate environment variables naming each one, and the Dockerfile's React stage is a Node build that asserts the output file exists before it proceeds.

The migration mechanism is the interesting part. Upgrades from versions where the mobile app owned the root path are handled by a narrowly scoped retirement worker served at a specific path. It replaces only the old root worker, removes the caches that the mobile build created, unregisters itself, and reloads a controlled window. The new frontend performs a one-time check for that exact root registration. Other registrations are explicitly left untouched, and the stated reason is to preserve a path for future offline support.

That is careful work for the failure mode nobody notices, which is an old worker serving stale assets over the top of a new application. It also means the removal of the legacy interface is a future event the page has already scheduled, and anyone still on it is on a path with a stated expiry.

## The web app and the task worker get the same one gigabyte limit

The production compose file defines two shared anchors, one for the task worker and one for the application, and they are nearly identical.

Both use the same image, the same environment file, the same bind-mounted data directory, the same dependency conditions on the database and the broker reporting healthy, the same restart policy, and the same log rotation with a fifty megabyte cap and five files. Both are attached to the same network and both wait for the same two services.

Both also carry the same resource block: a limit of one CPU and one gigabyte of memory, with a reservation of 256 megabytes.

The application process is a web server. The worker process is the one that decodes uploaded images, handles HEIC stills, drives media tooling and runs import and export jobs. Those are different workloads, and the second one is the one that allocates memory proportional to the size of the file it is processing.

A one gigabyte ceiling on the worker is a hard limit rather than a target, so a batch of large video uploads will not slow down, it will be killed. The page does not discuss sizing, so if you are processing real media the memory limit on the worker is the first thing to raise.

The broker is a Redis-compatible server, and the compose file points three separate URLs at it on database zero: the general Redis URL, the task broker URL, and the result backend URL. The database driver is set to the PostgreSQL one, and the file's header recommends PostgreSQL deployments over SQLite even though both are supported by separate compose files.

## The build rejects a GPL FFmpeg in the stage that installs one

The container image is built in three stages, and the middle one has a licence check that can stop the build.

That stage installs a list of system packages, and the media tooling is among them. Then it runs a check on the installed media tool's reported configuration, and if that output matches a GPL or nonfree pattern the stage echoes a failure and exits non-zero. Otherwise it echoes that an LGPL build was verified.

So the build has a deliberate licence gate on the media dependency, which is a good instinct for a project that ships a container. The tension is that the package that triggers the gate is installed two lines earlier in the same instruction, from the distribution's own repository, and the page does not say which build of that package satisfies the check.

The rest of that stage is worth reading for the detail. The dependency manager is copied from a versioned container image tag, so the package manager is pinned, while the Python and Node base images above and below it use floating distribution tags. The sync command runs against the committed lockfile, skips building the project as an editable install, and excludes the project itself, so the application is installed in a later step. And an environment variable is set specifically so that the virtual environment's symbolic links point at a system location that survives being copied into the final image, which is the standard way that a copied virtual environment breaks.

## The licence is named nowhere and there is a third-party licence file

The licence section of the readme is one sentence and it names nothing. It says the project is licensed under the terms specified in the licence file.

The repository platform reports no recognised licence for the repository, and the root contains a file with the conventional name in the two-part form, plus a separate file collecting third-party licences.

So a reader looking for permission has three things and no answer: a sentence pointing at a file, a platform that could not parse it, and a second file that covers other people's code. The dependency list is long enough, with thirty-eight exact pins including a JWT library, a password hashing library, a cryptography library, an OAuth library and a PDF renderer, that the third-party file has real work to do.

The dependency pinning is itself worth a note, because it is unusual. All thirty-eight runtime dependencies are pinned to exact versions with no ranges, and the package manager is configured to add upper bounds. For a self-hosted application whose data has to survive upgrades, exact pins are defensible; for anyone who wants to pick up a security fix without a release, they mean waiting for the maintainer.

One loose thread in the same file: the development group pins a type checker at a prerelease version in the zero point zero series, which is an alpha being used on the project's own source.

## Nine compose files, two frontend directories and committed test output

The repository root tells you what kind of project this is before the readme does.

There are nine compose files: a production one on PostgreSQL, a development one, SQLite variants of both, an override for continuous integration, an override for older installations, and three that add a sidecar component. There is a Kubernetes directory, an environment template, a database migration configuration with its own directory and a checked-in initial SQL file, and a task runner configuration file.

There are two frontend directories, which matches the two compiled frontends the Dockerfile produces: one built by the Node stage into a path named for the current application, and a second path named for the legacy application that the runtime stage is told to look in.

There is a test results directory committed at the root, which means test output is version controlled rather than ignored.

And there is a small executable file at the root whose name is not a directory and not a configuration file, plus two agent-tooling entries, one of which configures an automated review bot.

The beta count explains most of the layout. The newest release is the twenty-sixth iteration of the same pre-release version number, so a project at that stage accumulates deployment variants, an old-installation override and a sidecar option, and the readme's advice to keep regular backups exists because the developers say breaking data changes may still occur.

## Conclusion

journiv is a single-maintainer project and the readme is unusually honest about that, which is worth more than most feature lists. The disclaimer explains exactly how AI coding tools were used, states that review depth differs by area, and separates development-time assistance from runtime behaviour with a specific claim, that no journal content is sent to a model provider because the product does not require one at runtime. The deployment documentation is also candid: the production compose file defaults to failing closed on cookie auth over plain HTTP, and the note about the one-command install says in as many words that it starts a minimal version.

What to fix before you deploy anything. The one-command install passes a literal placeholder as the secret key, so every copy of the readme shares one; the production compose file carries the correct instruction, including the command that generates a proper random value, so use that path rather than the shortcut. The same shortcut deliberately enables the insecure cookie mode, which the page warns exposes passwords and refresh cookies to interception, and it publishes the port on all interfaces. And it starts no broker or worker, so media uploads and import or export jobs do not complete.

On capacity, the compose file gives both the web application and the task worker the same one CPU and one gigabyte limits, and the worker is the process that decodes images and drives media tooling. If you are processing real media, raise the worker's memory before you lower anything else.

## FAQ

### How do I install journiv quickly?

With one docker run that publishes port 8000, mounts a named data volume, sets a secret key, a domain name and an HTTP scheme opt-in, and pulls the published image. That command starts a minimal version: media processing and import or export need a broker and a worker, so the complete compose file is required for those.

### Is journiv open source and what licence is it under?

The readme says only that it is licensed under the terms specified in the licence file. The repository root has a licence file and a separate file for third-party licences, and the repository platform reports no recognised licence, so the readme names no licence at all.

### Does journiv send my journal entries to an AI provider?

The readme says no, and separates the two things explicitly: it states that the product does not require a language model service to run, and that using AI coding tools during development does not cause journal entries or other private content to be sent to an AI provider.

### Which database should journiv use?

The production compose file recommends PostgreSQL over SQLite and sets the database driver accordingly. SQLite variants of the compose files exist for lighter installs, and the application supports both a synchronous and an asynchronous driver.

### Why do media uploads stay in a processing state?

Because there is no task worker. The minimal one-command install starts only the web application, so images, video and audio stay in processing and never appear in entries, and import and export jobs are queued but never run. The complete compose file starts a worker alongside the app.

### What frontends does journiv serve?

Two, from one FastAPI origin. A React frontend owns the root path and is the default, and a legacy Flutter frontend remains available at a separate path temporarily and may be removed in a future release. A narrowly scoped service worker at a fixed path removes the old root worker on upgrade.

## Sources

- [Issues](https://github.com/journiv/journiv-app/issues)
- [journiv/journiv-app on GitHub](https://github.com/journiv/journiv-app)
- [Project website](https://www.journiv.com)
- [README](https://github.com/journiv/journiv-app/blob/main/README.md)
- [Releases](https://github.com/journiv/journiv-app/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/journiv-journiv-app
