By Marc Nguyen and Jean-Baptiste Rubio.
Specifications are given here: Protos and docs
Deploy an identity provider (like Dex):
# dex.config.yaml
# TODO: for production, set this to the public URL of the auth server
issuer: http://dex.example.com:5556
# TODO: for production, change this
storage:
type: memory
web:
http: 0.0.0.0:5556
telemetry:
http: 0.0.0.0:5558
# Configuration for static clients
staticClients:
# Used for login using server-side logic
- id: train-station
redirectURIs:
# TODO: for production, change this to the public URL of the front end
- 'http://train.example.com:5173/auth/callback'
name: 'Train Station'
secret: zYXYZSgEba6usrvj6lsjX5zQHEwaEi6mVbC5ulAlJ7zyV5QMzEdRYNoPZJnparTs
public: false
# Used for introspection
- id: train-station-api
name: 'Train Station API'
secret: xo72oHz1Re11Clz7jHbtWjaILQzqOSNK3WLmsAnBug2YazxdqXRdhtPyhgdBRBIY
public: false
# Used for login using client-side logic
- id: train-station-app
redirectURIs:
- com.example.trainstationapp://oauth2
name: Train Station App
public: true
enablePasswordDB: true
staticPasswords:
- email: '[email protected]'
# bcrypted "password"
hash: '$2b$12$acCCsOuwI09Lg81y5A/w2egiCLcPu934ct4TAgBHgzfahut.9Oir6'
username: 'admin'
userID: '08a515ad-1111-2222-3333-1234567890ab'
Setup the environment variables in the .env file for the API:
# LISTEN_ADDRESS=:3000
# TLS_KEY=
# TLS_CERT=
# TLS_CLIENT_CA=
INTROSPECTION_CLIENT_SECRET=xo72oHz1Re11Clz7jHbtWjaILQzqOSNK3WLmsAnBug2YazxdqXRdhtPyhgdBRBIY
INTROSPECTION_CLIENT_ID=train-station-api
INTROSPECTION_URL=http://dex.example.com:5556/token/introspect
# INTROSPECTION_CACHE_PERIOD
Then, deploy the app:
services:
init-permissions:
image: registry-1.docker.io/library/busybox:1.37.0-uclibc
volumes:
- store:/data
entrypoint: ['sh', '-c']
command:
- chown -R 1000:1000 /data && chmod -R 700 /data
train-station-api:
build:
context: .
dockerfile: Dockerfile
user: '1000:1000'
ports:
- '3000:3000'
env_file:
- .env
environment:
- DB_PATH=/data/db.sqlite3
volumes:
- store:/data
depends_on:
init-permissions:
condition: service_completed_successfully
dex:
image: ghcr.io/dexidp/dex:latest
ports:
- '5556:5556'
command: dex serve /etc/dex/config.yaml
volumes:
- ./dex.config.yaml:/etc/dex/config.yaml
volumes:
store:
Run the app:
docker compose up -d
Just use docker-compose to deploy the development environment:
cd /go
docker compose up -d --build
flowchart TD
subgraph server[ConnectRPC server]
healthAPIHandler
stationAPIHandler
end
Introspection --> stationAPIHandler
DB --> stationAPIHandler
healthAPIHandler is used to check the health of the server.stationAPIHandler is used to manage the stations (set favorite, get many,
etc.)introspection is used to introspect incoming JWT token, and to check if the
token is valid.Since we use introspection, we do not use JWKS to check if the token is valid.
erDiagram
Station }|..|{ User : favorite
Start the backend:
cd ./go
docker compose up -d
Install bun and install the dependencies:
bun install --frozen-lockfile
bun run dev
The Data layer:
StationsPagingSource.
The PagingSource is able to load pages of data stored in a
PagingData.Stations. It needs a JWT token to fetch datas.codeVerifierDataStore. The access token is cached inside the oauthDataStore.StationRepository and executes CRUD methods.Station of the response is cached and
returned.watch/watchOne), we observe the cache and may fetch
the initial values from a data source.Pager
to retrieve the PagingData from the cache. The pager uses the
StationRemoteMediator which is responsible to fetch and cache pages of
Station from a data source.In the Domain layer:
stationRepository satisfies most use cases (displaying a list
of Stations, displaying details of a Station, updating a Station...).In the Presentation layer :
ViewModels. The ViewModels act as the middle man
between the presentation layer and domain layer. This is to follow the
Modern Android App
Architecture.MainActivity renders a Scaffold with its TopAppBar. Inside that
scaffold is a NavigationHost composable.NavigationHost renders a page based on a route:/login, and shows a login button. The button triggers
a redirection to the OAuth provider, which then send the resulting OAuth
Access Token to the MainActivity and triggers the authAPI to fetch a
JWT. Upon receiving a JWT, the user is authenticated and is redirected to
the /stations route./stations route shows a LazyColumn which listen to a
Flow<PagingData<Station>>. This allows lazy loading of the data, and
therefore, the lazy loading of "station cards". The page also shows a
"About" page. When the user push on a "station card", the user is redirected
to the /details route./details route shows the position of the train station on Google Maps
and details about that station on a Bottom Sheet.MIT License
Copyright (c) 2026 Marc NGUYEN, Jean-Baptiste RUBIO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.