GO Version: 1.23.5
Type: Progressive Web Application (PWA)
This project started as a side project to build a simple travel website for one particular user. What began as a simple CRUD app has evolved into a unified Progressive Web App (PWA) that serves as both a responsive website and an installable mobile application - providing the best of both worlds with a single codebase.
π Works as a Website: Full desktop experience with rich UI and all features
π± Works as a Mobile App: Installable on phones, offline support, native-like features
π§ Single Codebase: One application that adapts to any device or platform
This application is written in Go/HTMX/Templ and provides comprehensive trip management capabilities. The backend uses the built-in net/http package with a SQLite database, real-time flight APIs, and modern PWA features that enhance both web and mobile experiences.
- Responsive Design: Adapts to any screen size (desktop, tablet, mobile)
- Full Feature Set: Complete trip management, statistics, world map
- Desktop Navigation: Traditional top navigation bar
- Rich Interactions: HTMX-powered dynamic content updates
- Installable: Add to home screen on iOS, Android, or desktop
- Bottom Navigation: Touch-optimized mobile navigation bar
- Offline Support: View cached trips and sync when online
- Native Features: Geolocation, camera access, pull-to-refresh
- Background Sync: Queue actions when offline, sync when connected
- App Shortcuts: Quick actions from home screen icon
The same application provides different experiences based on the device:
- Desktop browsers: Full website experience
- Mobile browsers: Mobile-optimized website
- Installed on mobile: Native app-like experience with PWA features
- Offline mode: Core functionality available without internet
- AviationStack API: Real-time flight/airport information Visit Site | API Docs
- PWA Testing: Use PWABuilder for validation
- Mobile Testing: Chrome DevTools Device Mode or real devices via HTTPS
This application uses the following middleware
- Auth
- TextHTML Middleware (serving html from the backend)
- CSP Middleware
- Logging
The CSP Middleware is used as a security measure to prevent unexpected <script> tags, inline JS code, and external resources such as images, fonts, etc. Since HTMX dynamically swaps html into the page via AJAX this is especially important in production environment to prevent XSS (cross-site scripting) and similar attacks.
Dev Note I have tried using chaining middleware. For some reason though it seems to break CSP And TextHTML Middleware effectively making the app inoperable. It is something I wish to tackle in the future.
The sqlite database currently uses the following tables (all which can be found under internal/database/schema.sql)
- airports: Static list of all airports. Populated using csv files received from public websites
- users: Holds user information
- trips: Holder all trip information. Many queries will pair this with airports via a
JOINoperation - sessions: Holds session data of the user.
- password_reset_tokens: Holds 1 hour expiry reset tokens for users requesting forgot-password
All .go files under the database serve to run SQL queries on their respective tables and pass them to the handlers.
THe files in this folder represent the backend of the project. The naming convention for these files generally fall under this ruleset:
{get/post/update/delete}{resource_name}.go
For example, to create a handler which is called by a GET request to retrieve the home page, we name the handler gethome.go
Each handler also follows the following file structure so that it can be easily called in the mux handler:
// Base Handler Struct. Any database structs are defined here
type DeleteTripHandler struct {
tripStore *db.TripStore
}
// The Handler Params struct
type DeleteTripHandlerParams struct {
TripStore *db.TripStore
}
// This function allows the mux in main.go to pass in the actual database services
func NewDeleteTripHandler(params DeleteTripHandlerParams) (*DeleteTripHandler) {
return &DeleteTripHandler{
tripStore: params.TripStore,
}
func (h *NewDeleteTripHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handler Logic Here
}The templates folder stores all the htmx/templ files. The general convention here is that if a component exist or will populate the same page they must live in the same file.
For example, all trip related components (RenderPastTrips, CreateTripPage) all live under trips.templ. This is to organize and easily find where all the components are.
In addition, for the page to appear under tha layout the layout must be instantiated in the handler servicing the page and then rendered with the component to go inside. Example:
internal/handlers/gettrip.go
c := templates.TripsPage()
templates.Layout(c, "Trips").Render(r.Context(), w)- htmx.min.js: Core HTMX library for dynamic content
- convertTimes.js: UTC time standardization and timezone handling
- leaflet.js: Map library for world map visualization
- map.js: Map configuration and markers
- modal.js: Trip form show/hide logic
- response-targets.js: HTMX response targeting
- tabs.js: Sliding animation logic
- pwa-features.js: PWA functionality (geolocation, camera, pull-to-refresh, offline sync)
- sw.js: Service worker for caching and offline support
- output.css: Generated Tailwind CSS
- mobile.css: Mobile-optimized styles and touch-friendly UI
- leaflet.css: Map styling
- manifest.json: Web app manifest with metadata and icons
- static/icons/: Complete icon set (72x72 to 512x512) for all devices
- Google Auth:
- google_auth.go: Sets up the auth config
- google_login.go: Sends a request to Google oauth
- google_callback.go: Once Google processes the request, the callback validates the user and sets the session for login
- Flights
- AviationStack has a free 100 calls/month plan which allows to call real time flight data. Here, we get the flight route using the iata_code and request the user to enter in the date.
-
Go Installed: Ensure Go is installed on your system (at least 1.23). You can download it from here.
- Verify installation:
go version
- Verify installation:
-
Environment: You can set an optional
PORTenvironment variable to specify the port number on which the backend will run. SetBASE_PATH=/fromntoto mirror the production sub-path locally.
I use sqlite for this project
Assuming you're working on an Ubuntu 22.04, but this process is similar for most OS cases.
sudo apt updatesudo apt install sqlite3Verify the installation
sqlite3 --versionCreates the shell and new database. I like to put this under the database folder
sqlite3 database.dbYou can also load the schema.sql, which is store under the database folder
sqlite3 database.db < database/schema.sqlTo generate the Tailwind style sheet, we use the Tailwind binary. To get started with TailWind CSS, make sure you have the correct binary in the root directory. follow the instructions in this guide. Make sure you download the correct binary for your operating system. https://tailwindcss.com/blog/standalone-cli
Generating the output.css file
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watchAdd the href to the templ files
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Trips</title>
<script src="/static/js/htmx.min.js"></script>
<link rel="stylesheet" href="/static/css/output.css" />
</head>Generate the files via
templ generateFor PWA functionality, additional steps are required:
PWAs require HTTPS in production. For local testing:
Option A: Use ngrok for HTTPS tunnel
# Install ngrok, then start your app and create tunnel
air &
ngrok http 3000Option B: Local testing (limited PWA features)
# Find your local IP
ip addr show | grep "inet " | grep -v 127.0.0.1
# Access via http://YOUR_IP:3000 on mobile
# Note: PWA installation requires HTTPSIcons are auto-generated, but you can replace them:
# Icons are stored in static/icons/
# Replace with custom designs maintaining the same sizesAir is useful to autoload and track your go code
https://github.com/cosmtrek/air
Refere to Here for reference
First, init air if not already doe
air initThat wil create the .air.toml file. Then just run the air command.
air<button hx-get="/trips" hx-target="#trip-list" hx-swap="innerHTML">
Load Trips
</button>
<div id="trip-list">
<ul>
for _, trip := range trips {
<li>{ trip.Airline }</li>
}
</ul>
</div>- Clone the Repository:
git clone <repository_url>
cd trip-tracker- Generate Required Files:
# Generate Tailwind CSS
./tailwindcss -i ./static/css/input.css -o ./static/css/output.css
# Generate Templ templates
templ generate- Start Development Server:
air- Access the Application:
- Desktop (local dev): http://localhost:3000
- Desktop (sub-path test):
BASE_PATH=/fromnto airβ http://localhost:3000/fromnto - Mobile Browser: http://YOUR_IP:3000 (responsive mobile website)
- Production: https://themshin.com/fromnto
- Full PWA Features: Use ngrok tunnel (HTTPS required)
- Limited Features: http://YOUR_IP:3000 (no installation, but mobile UI)
β Website Testing (Desktop):
- Open http://localhost:3000 in any browser
- Test full desktop experience with top navigation
- Verify all features work (trips, statistics, world map)
β Website Testing (Mobile Browser):
- Open http://YOUR_IP:3000 on mobile device
- See responsive design with bottom navigation
- Test touch interactions and mobile layout
β Mobile App Testing (PWA):
- Use HTTPS (ngrok tunnel) for full PWA features
- Install prompt appears and app installs to home screen
- Test offline functionality and native-like features
- Verify background sync and app shortcuts work
-
Port Already in Use:
- Error:
listen tcp :3000: bind: address already in use - Solution: Use a different port by setting the
PORTenvironment variable:Another solution is to kill the process using port 3000export PORT=4000 go run main.go
- Error:
-
Invalid JSON Payload:
- Ensure the payload in
POSTorPUTrequests is well-formed and includes all required fields.
- Ensure the payload in
-
CSS Not Being Updated
- Ensure that your browser cache isn't caching an old version of your output.css file
We use docker and docker compose to run deployment.
The following forces the docker to build the Dockerfile and run in detached mode
docker compose up --build -dIt will setup the following
- The trip_tracker service with replicas
- The traefik reverse proxy
- Watchtower to check for new updates to docker images and then run rolling updates
Once you have it initially setup, watchtower will listen for any new updates to the docker image and update if there's a change to a tag
There is an example script docker_build.sh which will build the Dockerfile and then upload to this repository as a ghcr.io image.
For me I used Hostinger. They have good pricing and super easy to setup with some knowledge of LinuxOS (I personally used Ubuntu)
I also used this Youtube Video. Basically tells you how to setup a VPS from scratch. Highly recommend.
This project is licensed under the APACHE 2.0 License. See the LICENSE file for details.