Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Start


Revisit your travel memories

Open source travel documentation app for self-hosting or serverless usage
Lightweight and future proof


Try out the online version
Press the Load button on landing page to use example data.


Background

The idea for this application came to me while reflecting on the countless journeys I had taken. Despite all the places I had visited, I found myself forgetting key moments and locations. Friends would ask about my travels, and I often struggled to recall the details or find the photos that matched those memories. I realized I needed a way to not only document my trips but also retrieve the content of the documentation with ease.

That is how Immer in Bewegung was born. The name, meaning always in movement in German, perfectly encapsulates the heart of this project. To bring this concept to life, the frog is chosen as the mascot—a symbol of endless motion and adventure.

Note

You can check out the online app running at https://go.bewegung.app/. Click on Load if you want to try with example data.

Basic idea

Over time, most code in applications becomes obsolete. To ensure your memories are not lost to such obsolescence, the core idea of this project is to visualize travel data stored in the JSON format. JavaScript Object Notation (JSON) is an incredibly structured way to store data, offers long-term reliability, and is more likely to remain compatible with future technologies. This makes it a solid, future-proof solution. However, scrolling through a long JSON file directly can be tedious—so why not build an interface on top? But this project is not just an application; it’s a versatile syntax that allows you to take travel notes in a simple and structured way.

Main features

  • A single JSON file with an easy-to-understand syntax
  • Use directly on GitHub or host it on your own server
  • Use in conjunction with Immich to combine your documentation with photos (optional)

img An idea from Sweden

Quick Start

Usage alternatives

  • Serverless
  • Self-hosted

Serverless usage (simplest choice)

Download the database template (read more about how to edit the file under Create the dataset).

https://github.com/plans-coding/immer-in-bewegung/raw/refs/heads/main/bewegung.json

Go to https://go.bewegung.app/ and bind the downloaded file to the application.

Host on your own server

If you want your app available from many devices without having to bind the file specifically on each device, you can deploy your instance to your own server instead. Go to the directory where you want to place your files, e.g.

cd /var/www/bewegung

Download the latest release and extract it into that folder. Then point a web server at the directory and open it in your browser. For exposing the server to the internet (and optional authentication), see Other needs.

Create the dataset

Data Structure

AttributeLevelDescription
SettingsRootThis is the object that contains all app settings.
TripsRootThis is an array object where each child represents a single trip.

For more detailed instructions, follow the Trip Syntax.

Tip

Download a pre-filled JSON template file.

Note

To obtain coordinates for a given position, you can use the Coordinate Tool and easily copy and paste the coordinates into the Editor in the app.

Visualisation

Mermaid diagram

Trip Syntax

Trip Object

Each trip object has fields according to the following. The key of each trip object is the InnerId — a six-character hexadecimal string (characters 09 and af) generated randomly when the trip is created. You do not set it manually.

Note

Date, time and coordinate fields below should follow the stated format so they display correctly and feed the statistics. The editor auto-formats most of these, but the format is not strictly enforced — malformed values may simply be ignored (e.g. a pin with an unparseable coordinate is not plotted).

FieldDescription
TripDomainThe category of the trip. Allowed values are the keys defined in TripDomainColors (see Settings); by default Domestic, Abroad, or Attachment.

Syntax: <one of the defined trip domains>
ArrangerGroupWho arranged the trip, e.g. Family, Private, or Work. Allowed values are the entries in ArrangerGroups (see Settings).

Syntax: <one of the defined arranger groups>
OuterIdThe OuterId is the trip identifier exposed to the end-user of the web app. In the editor it is auto-filled as the first letter of TripDomain plus the first letter of ArrangerGroup (uppercased), followed by a dash and a sequential number per prefix — e.g. a Domestic Family trip becomes DF-1, the next DF-2. You can override it with any value.

Syntax: XX-N (auto), or a custom string.
OverallDestinationType in you trip primary destination. E.g. Finland, or Italia, Spain etc.

Syntax: <string>
DepartureDateYour departure date in ISO format.

Syntax: YYYY-MM-DD
ReturnDateYour return date in ISO format.

Syntax: YYYY-MM-DD
TripLabelsAdd keywords to the trip for filter purposes.

Syntax: LabelA, LabelB, LabelC, ...
MapPinsThe main stops of your trip that you want to plot on the map. One pin per line.

Syntax (multi-line accepted): [ NAME_OF_MAP_PIN ]( latitude, longitude ) — decimal degrees, comma-separated.
StartNodeWhere the trip started. N.B. Not exposed in the web app — for your own records.

Syntax: <string>
EndNodeWhere the trip ended. N.B. Not exposed in the web app — for your own records.

Syntax: <string>
TripDescriptionA short description that explain the aim of the trip. E.g. My fantastic summer trip to France.

Syntax: <string>
PhotoStarttimeFor use with Immich. If you leave your home at let say 8 pm and don’t want to include photos from earlier on departure day, then you can set a time here. If left empty, all photos from departure day will be included.

Syntax: HH:MM
PhotoEndtimeFor use with Immich. Same as above, but for return day.

Syntax: HH:MM
PhotoAlbumsFor use with Immich. Names of Immich albums to link from the trip. Each [ Album Name ] becomes a button that opens the matching album in Immich. You can also add !desc[ text ] to create a button that searches Immich for photos with that description.

Syntax (multi-line accepted): [ Album Name ] !desc[ description ]
CoverPhotoThe cover image of the trip, shown in Cards view and in the report. Provide a direct image URL.

Syntax: <image URL>
DocumentationNoteFree-text note for the whole trip. N.B. Not exposed in the web app — for your own records.

Syntax (multi-line accepted): <string>
DaysAn array containg each day and their related data. See below.

Trip Day Object

There is a second level array object (Days) in the trip object with fields according to following.

Note

Date and coordinate fields below should follow the stated format so they display and map correctly. The format is not strictly enforced — malformed values are simply ignored where they can’t be parsed.

FieldDescription
DateYour date in ISO format.

Syntax: YYYY-MM-DD
EventsThis is your fully description of what you did this day.

Syntax (multi-line accepted): <string>
AccommodationThe name and the address of your accommodation.

Syntax: <string>
AccommodationCountryThe name of the country you stayed in during the night.

Syntax: <string>
AccommodationCoordinatesThe GPS coordinates of your accommodation in decimal degrees, e.g. 59.329444, 18.068611. The space after the comma is optional.

Syntax: latitude, longitude
AccommodationCoordinatesAccuracyFree-text note on the accuracy of the coordinates, shown next to the accommodation, e.g. exact or approximate. Optional.

Syntax: <string>
TravelParticipantsThe name of your travel buddies, separated by comma. N.B. This is not exposed in web app.

Syntax: <string>
AdditionalNotesN.B. Additional notes are not exposed in web app.

Syntax (multi-line accepted): <string>
CountriesDuringDayThis field is for enhanced statistics. Enter all countries in chronological order during that day, separated by comma. The country names need to conform to the country names defined in ContinentCountries. In front of a country name the prefixes *, **, and + are allowed. Read more under Special syntax used for Countries During Day (Events).

Syntax (prefix allowed): <country name without space>, <country name without space>, <country name without space> [...]

Special syntax used for Countries During Day (Events)

PrefixFunction
*Shorter visit, but of significant importance
**Very short visit, without significant importance
+Re-entry / pass-through of a country. Like **, it is not added to your count of unique visited countries.

Note

Countries During Day: Using a prefix in front of a country name in the CountriesDuringDay field of a day affects how that country is counted on the Statistics page.

Settings

The following settings are applicable

Attribute GroupAttributeValueDescription
BaseHomeContinentContinentUse this to sort your home continent first.
BaseHomeCountryCountryUse this to remove your own country from the trip unique countries field. Use same spelling as in your notes and in the value of ContinentCountries.
BaseLanguageFileFilenameFilename to the translation file located in the languages folder, e.g. swedish.json.
DefinitionArrangerGroupsListName of the arranger groups you want to use.
DefinitionContinentCountriesListDefinitions of continents and countries as well as their language (and spelling).
DefinitionTripDomainColorsListColor definitions of your trip domains.
BaseImmichDisabled or EnabledEnable if you want filter links from all event dates to Immich.
PhotosImmichApiKeyKeyAPI key generated in Immich, used to authenticate the photo requests.
PhotosImmichCoverAlbumIdGUIDThe GUID of the cover photo album in Immich.
PhotosImmichUrlURLOnline URL to the Immich installation (e.g. https://immich.example.com/).
OtherExternalMapProviderURLSet your prefered map provider.

Arranger Group

A categorisation of the arrangers of different trips.

Trip Domains

There are three pre-defined trip domains

  • Domestic trip
  • Trip abroad
  • Attachment trip

An attachment trip is defined as a visit to a country where you have a deeper connection. For example, if you study abroad, you might want to document that time but distinguish it from the ordinary abroad or domestic category.

Set theme colors to your different TripDomains.

Domestic: #0b5394
Abroad: #1d655e
Attachment: #C60C30

Continent Countries

If you want to change (make other country definitions or translate country names) the pre-defined country settings you can do it by changing in the country definitions. Only countries defined in the table ContinentCountries can be used in your trip notes and only with the very exact spelling.

Tip

Other language: If you have the countries in your Trip notes written in another languange than English, then you can change the app behaviour by translating the countries in ContinentCountries to your own language.

{
  "Africa": {
    "Algeria": "DZ",
    "Angola": "AO",
    "Benin": "BJ",
    "Botswana": "BW",
    "Burkina-Faso": "BF",
    "Burundi": "BI",
    "Cabo-Verde": "CV",
    "Cameroon": "CM",
    "Central-African-Republic": "CF",
    "Chad": "TD",
    "Comoros": "KM",
    "Democratic-Republic-of-the-Congo": "CD",
    "Djibouti": "DJ",
    "Egypt": "EG",
    "Equatorial-Guinea": "GQ",
    "Eritrea": "ER",
    "Eswatini": "SZ",
    "Ethiopia": "ET",
    "Gabon": "GA",
    "Gambia": "GM",
    "Ghana": "GH",
    "Guinea": "GN",
    "Guinea-Bissau": "GW",
    "Ivory-Coast": "CI",
    "Kenya": "KE",
    "Lesotho": "LS",
    "Liberia": "LR",
    "Libya": "LY",
    "Madagascar": "MG",
    "Malawi": "MW",
    "Mali": "ML",
    "Mauritania": "MR",
    "Mauritius": "MU",
    "Morocco": "MA",
    "Mozambique": "MZ",
    "Namibia": "NA",
    "Niger": "NE",
    "Nigeria": "NG",
    "Republic-of-the-Congo": "CG",
    "Rwanda": "RW",
    "Sao-Tome-and-Principe": "ST",
    "Senegal": "SN",
    "Seychelles": "SC",
    "Sierra-Leone": "SL",
    "Somalia": "SO",
    "South-Africa": "ZA",
    "South-Sudan": "SS",
    "Sudan": "SD",
    "Tanzania": "TZ",
    "Togo": "TG",
    "Tunisia": "TN",
    "Uganda": "UG",
    "Zambia": "ZM",
    "Zimbabwe": "ZW"
  },
  "Asia": {
    "Afghanistan": "AF",
    "Armenia": "AM",
    "Azerbaijan": "AZ",
    "Bahrain": "BH",
    "Bangladesh": "BD",
    "Bhutan": "BT",
    "Brunei": "BN",
    "Cambodia": "KH",
    "China": "CN",
    "Cyprus": "CY",
    "Cyprus-Northern-Cyprus": null,
    "Georgia": "GE",
    "India": "IN",
    "Indonesia": "ID",
    "Iran": "IR",
    "Iraq": "IQ",
    "Israel": "IL",
    "Japan": "JP",
    "Jordan": "JO",
    "Kazakhstan": "KZ",
    "Kuwait": "KW",
    "Kyrgyzstan": "KG",
    "Laos": "LA",
    "Lebanon": "LB",
    "Malaysia": "MY",
    "Maldives": "MV",
    "Mongolia": "MN",
    "Myanmar": "MM",
    "Nepal": "NP",
    "North-Korea": "KP",
    "Oman": "OM",
    "Pakistan": "PK",
    "Philippines": "PH",
    "Qatar": "QA",
    "Russia": "RU",
    "Saudi-Arabia": "SA",
    "Singapore": "SG",
    "South-Korea": "KR",
    "Sri-Lanka": "LK",
    "Syria": "SY",
    "Taiwan": "TW",
    "Tajikistan": "TJ",
    "Thailand": "TH",
    "Timor-Leste": "TL",
    "Turkey": "TR",
    "Turkmenistan": "TM",
    "United-Arab-Emirates": "AE",
    "Uzbekistan": "UZ",
    "Vietnam": "VN",
    "Yemen": "YE"
  },
  "Europe": {
    "Albania": "AL",
    "Andorra": "AD",
    "Austria": "AT",
    "Belarus": "BY",
    "Belgium": "BE",
    "Bosnia-and-Herzegovina": "BA",
    "Bulgaria": "BG",
    "Croatia": "HR",
    "Cyprus": "CY",
    "Cyprus-Northern-Cyprus": null,
    "Czech-Republic": "CZ",
    "Denmark": "DK",
    "Denmark-Faraoe-Islands": "FO",
    "Estonia": "EE",
    "Finland": "FI",
    "Finland-Åland": "AX",
    "France": "FR",
    "Georgia": "GE",
    "Germany": "DE",
    "Greece": "GR",
    "Hungary": "HU",
    "Iceland": "IS",
    "Ireland": "IE",
    "Italy": "IT",
    "Kosovo": "XK",
    "Latvia": "LV",
    "Liechtenstein": "LI",
    "Lithuania": "LT",
    "Luxembourg": "LU",
    "Malta": "MT",
    "Moldova": "MD",
    "Moldova-Transnistria": null,
    "Monaco": "MC",
    "Montenegro": "ME",
    "Netherlands": "NL",
    "North-Macedonia": "MK",
    "Norway": "NO",
    "Poland": "PL",
    "Portugal": "PT",
    "Romania": "RO",
    "Russia": "RU",
    "San-Marino": "SM",
    "Serbia": "RS",
    "Slovakia": "SK",
    "Slovenia": "SI",
    "Spain": "ES",
    "Sweden": "SE",
    "Switzerland": "CH",
    "Ukraine": "UA",
    "United-Kingdom": "GB",
    "United-Kingdom-Akrotiri-and-Dhekelia": "GB",
    "United-Kingdom-Gibraltar": "GI",
    "United-Kingdom-Jersey": "JE",
    "United-Kingdom-Northern-Ireland": "GB",
    "Vatican-City": "VA"
  },
  "North-America": {
    "Antigua-and-Barbuda": "AG",
    "Bahamas": "BS",
    "Barbados": "BB",
    "Belize": "BZ",
    "Canada": "CA",
    "Costa-Rica": "CR",
    "Cuba": "CU",
    "Denmark-Greenland": "GL",
    "Dominica": "DM",
    "Dominican-Republic": "DO",
    "El-Salvador": "SV",
    "Grenada": "GD",
    "Guatemala": "GT",
    "Haiti": "HT",
    "Honduras": "HN",
    "Jamaica": "JM",
    "Mexico": "MX",
    "Nicaragua": "NI",
    "Panama": "PA",
    "Saint-Kitts-and-Nevis": "KN",
    "Saint-Lucia": "LC",
    "Saint-Vincent-and-the-Grenadines": "VC",
    "Trinidad-and-Tobago": "TT",
    "USA": "US"
  },
  "Oceania": {
    "Australia": "AU",
    "Fiji": "FJ",
    "Kiribati": "KI",
    "Marshall-Islands": "MH",
    "Micronesia": "FM",
    "Nauru": "NR",
    "New-Zealand": "NZ",
    "Palau": "PW",
    "Papua-New-Guinea": "PG",
    "Samoa": "WS",
    "Solomon-Islands": "SB",
    "Tonga": "TO",
    "Tuvalu": "TV",
    "Vanuatu": "VU"
  },
  "South-America": {
    "Argentina": "AR",
    "Bolivia": "BO",
    "Brazil": "BR",
    "Chile": "CL",
    "Colombia": "CO",
    "Ecuador": "EC",
    "Guyana": "GY",
    "Paraguay": "PY",
    "Peru": "PE",
    "Suriname": "SR",
    "Uruguay": "UY",
    "Venezuela": "VE"
  }
}

Translations

App translation

If the app is not available in your language you can easily translate it by placing a file in the folder languages/ and change the language under Settings.

Read more under Settings.

Continent and Country translations

The continent and countries can be translated, read more Continent Countries.

Trip Domain translation

The trip domains can be translated, read more Trip Domains.

Immich Integration

Tip

When Immer in Bewegung embeds photos in image view (see below), the search bar from Immich will be displayed unless you apply a small CSS modification in your Immich settings.

Add

body:has(#search-chips:target) #asset-selection-app-bar,
body:has(#search-chips:target) #search-chips {
  display:none;
}
body:has(#search-chips:target),
body:has(#search-chips:target) #search-content {
  background-color: #000000;
}

to User Icon > Administration > Settings > Theme Settings: Custom CSS.

Usage of Immich API

To make use of Immich API you need to allow CORS, example snippet to add to /etc/caddy/Caddyfile

(cors) {
        @cors_preflight{args[0]} method OPTIONS
        @cors{args[0]} header Origin {args[0]}

        handle @cors_preflight{args[0]} {
                header {
                        Access-Control-Allow-Origin "{args[0]}"
                        Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
                        Access-Control-Allow-Headers *
                        Access-Control-Max-Age "3600"
                        defer
                }
                respond "" 204
        }

        handle @cors{args[0]} {
                header {
                        Access-Control-Allow-Origin "{args[0]}"
                        Access-Control-Expose-Headers *
                        defer
                }
        }
}

immich.your-server-name.dedyn.io {

        # Needed for API call
        import cors https://immich.your-server-name.dedyn.io

        # Needed for iframe embedding
        header {
                Cross-Origin-Embedder-Policy require-corp
                Cross-Origin-Resource-Policy cross-origin
        }

        @options method OPTIONS
        respond @options 204

        reverse_proxy http://localhost:2283

}

Other needs

Expose web server to internet

Using Caddy this becomes easy. Deploy the server with

immich.your-server-name.dedyn.io {

        # Generate password storing hash with: caddy hash-password
        basic_auth {
                # Username "iib", password "bewegung"
                iib $2a$14$basROD3Y0cLE.VqXd.h89.akCQDKzhp6IH9ND2CRFyEICkMrKn3AO        
        }

        root /var/www/bewegung/

}

Alternatives

  • Add support for https
    • E.g. via Let’s encrypt
  • Use user authentication via
    • Authelia, or
    • Authentik, or
    • Client certificates, or
    • Port forwarding over SSH (e.g. using Termius on Android)

Usage of SQLite

Earlier editions of this application featured SQLite as the storage format instead of JSON. If you want to make a copy of your JSON documentation and store it in SQLite you can use the conversion tool located at https://go.bewegung.app/extra/json2sqlite.html.

SQLite

Logotype

If you like Immer in Bewegung and want to link to the project from your homepage, you can use this button.

img

Code for embedding to copy

<a href="https://bewegung.app/" style="text-decoration:none;"><img src="img/get_bewegung.svg"></a>