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)
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
| Attribute | Level | Description |
|---|---|---|
| Settings | Root | This is the object that contains all app settings. |
| Trips | Root | This 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
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 0–9 and a–f) 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).
| Field | Description |
|---|---|
| TripDomain | The 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> |
| ArrangerGroup | Who 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> |
| OuterId | The 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. |
| OverallDestination | Type in you trip primary destination. E.g. Finland, or Italia, Spain etc. Syntax: <string> |
| DepartureDate | Your departure date in ISO format. Syntax: YYYY-MM-DD |
| ReturnDate | Your return date in ISO format. Syntax: YYYY-MM-DD |
| TripLabels | Add keywords to the trip for filter purposes. Syntax: LabelA, LabelB, LabelC, ... |
| MapPins | The 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. |
| StartNode | Where the trip started. N.B. Not exposed in the web app — for your own records. Syntax: <string> |
| EndNode | Where the trip ended. N.B. Not exposed in the web app — for your own records. Syntax: <string> |
| TripDescription | A short description that explain the aim of the trip. E.g. My fantastic summer trip to France. Syntax: <string> |
| PhotoStarttime | For 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 |
| PhotoEndtime | For use with Immich. Same as above, but for return day. Syntax: HH:MM |
| PhotoAlbums | For 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 ] |
| CoverPhoto | The cover image of the trip, shown in Cards view and in the report. Provide a direct image URL. Syntax: <image URL> |
| DocumentationNote | Free-text note for the whole trip. N.B. Not exposed in the web app — for your own records. Syntax (multi-line accepted): <string> |
| Days | An 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.
| Field | Description |
|---|---|
| Date | Your date in ISO format. Syntax: YYYY-MM-DD |
| Events | This is your fully description of what you did this day. Syntax (multi-line accepted): <string> |
| Accommodation | The name and the address of your accommodation. Syntax: <string> |
| AccommodationCountry | The name of the country you stayed in during the night. Syntax: <string> |
| AccommodationCoordinates | The GPS coordinates of your accommodation in decimal degrees, e.g. 59.329444, 18.068611. The space after the comma is optional. Syntax: latitude, longitude |
| AccommodationCoordinatesAccuracy | Free-text note on the accuracy of the coordinates, shown next to the accommodation, e.g. exact or approximate. Optional. Syntax: <string> |
| TravelParticipants | The name of your travel buddies, separated by comma. N.B. This is not exposed in web app. Syntax: <string> |
| AdditionalNotes | N.B. Additional notes are not exposed in web app. Syntax (multi-line accepted): <string> |
| CountriesDuringDay | This 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)
| Prefix | Function |
|---|---|
* | 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 Group | Attribute | Value | Description |
|---|---|---|---|
| Base | HomeContinent | Continent | Use this to sort your home continent first. |
| Base | HomeCountry | Country | Use 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. |
| Base | LanguageFile | Filename | Filename to the translation file located in the languages folder, e.g. swedish.json. |
| Definition | ArrangerGroups | List | Name of the arranger groups you want to use. |
| Definition | ContinentCountries | List | Definitions of continents and countries as well as their language (and spelling). |
| Definition | TripDomainColors | List | Color definitions of your trip domains. |
| Base | Immich | Disabled or Enabled | Enable if you want filter links from all event dates to Immich. |
| Photos | ImmichApiKey | Key | API key generated in Immich, used to authenticate the photo requests. |
| Photos | ImmichCoverAlbumId | GUID | The GUID of the cover photo album in Immich. |
| Photos | ImmichUrl | URL | Online URL to the Immich installation (e.g. https://immich.example.com/). |
| Other | ExternalMapProvider | URL | Set 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
ContinentCountriesto 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
Hide Immich search bar
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.
Logotype
If you like Immer in Bewegung and want to link to the project from your homepage, you can use this button.
Code for embedding to copy
<a href="https://bewegung.app/" style="text-decoration:none;"><img src="img/get_bewegung.svg"></a>