Published: Nov 19, 2024 by Isaac Johnson
With Thanksgiving around the corner and Black-Friday emails flooding our inboxes, I thought I might check out some self-hosted open-source wish list apps.
We’ll set up and use Wishlist as well as Christmas Community and demonstrate their usage. We’ll also take a look at some decent hosted SaaS options as well.
Wishlist
For those wishing they had a nice self-hosted option for sharing wishlists that isn’t tied to some major retailer, Wishlist is worth checking out.
Let’s start with the Docker container first.
I’ll clone the repo and create a default .env file
builder@LuiGi:~/Workspaces$ git clone https://github.com/cmintey/wishlist.git
Cloning into 'wishlist'...
remote: Enumerating objects: 3821, done.
remote: Counting objects: 100% (1040/1040), done.
remote: Compressing objects: 100% (459/459), done.
remote: Total 3821 (delta 675), reused 735 (delta 566), pack-reused 2781 (from 1)
Receiving objects: 100% (3821/3821), 4.66 MiB | 881.00 KiB/s, done.
Resolving deltas: 100% (2085/2085), done.
builder@LuiGi:~/Workspaces$ cd wishlist/
builder@LuiGi:~/Workspaces/wishlist$ cp .env.example .env
builder@LuiGi:~/Workspaces/wishlist$ vi .env
builder@LuiGi:~/Workspaces/wishlist$ cat .env
# If behind a reverse proxy, set this to your domain
# i.e. https://wishlist.your-domain.org
ORIGIN=
# Hours until signup and password reset tokens expire
TOKEN_TIME=72
# The currency to use when a product search does not return a currency
DEFAULT_CURRENCY=USD
then I can docker compose up
builder@LuiGi:~/Workspaces/wishlist$ docker compose up
[+] Running 18/18
✔ app 17 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿] 0B/0B Pulled 129.0s
✔ a480a496ba95 Already exists 0.0s
✔ e28f673e04b0 Pull complete 0.5s
✔ c737a44a97c3 Pull complete 67.4s
✔ 9f58814a2227 Pull complete 1.3s
✔ b658bf75a5fb Pull complete 1.1s
✔ 55d88dbd8fba Pull complete 1.7s
✔ af681cf4541f Pull complete 76.2s
✔ a750316261aa Pull complete 2.1s
✔ c2d0deb96343 Pull complete 2.6s
✔ 7721b64b7474 Pull complete 21.1s
✔ dcbec1c1fafd Pull complete 24.1s
✔ ed38be4ab95f Pull complete 121.6s
✔ d6beba06d742 Pull complete 68.3s
✔ 29522d2270e1 Pull complete 68.8s
✔ 8dbeb448d174 Pull complete 69.3s
✔ 19ddcfdbfd36 Pull complete 79.3s
✔ 4f4fb700ef54 Pull complete 76.6s
[+] Running 2/2
✔ Network wishlist_default Created 0.1s
✔ Container wishlist-app Created 0.4s
Attaching to wishlist-app
wishlist-app | {"level":"info","ts":1731623735.5402846,"msg":"using config from file","file":"/usr/src/app/Caddyfile"}
wishlist-app | {"level":"info","ts":1731623735.5428708,"msg":"adapted config to JSON","adapter":"caddyfile"}
wishlist-app | {"level":"warn","ts":1731623735.542908,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/usr/src/app/Caddyfile","line":2}
wishlist-app | {"level":"warn","ts":1731623735.5432708,"logger":"admin","msg":"admin endpoint disabled"}
wishlist-app | {"level":"info","ts":1731623735.5437567,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0006a1c00"}
wishlist-app | {"level":"info","ts":1731623735.5438497,"logger":"http.auto_https","msg":"automatic HTTPS is completely disabled for server","server_name":"srv0"}
wishlist-app | {"level":"info","ts":1731623735.5452514,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
wishlist-app | {"level":"info","ts":1731623735.5458977,"msg":"autosaved config (load with --resume flag)","file":"/root/.config/caddy/autosave.json"}
wishlist-app | {"level":"info","ts":1731623735.5459216,"msg":"serving initial configuration"}
wishlist-app | Successfully started Caddy (pid=18) - Caddy is running in the background
wishlist-app | {"level":"info","ts":1731623735.552621,"logger":"tls","msg":"cleaning storage unit","storage":"FileStorage:/root/.local/share/caddy"}
wishlist-app | {"level":"info","ts":1731623735.5529034,"logger":"tls","msg":"finished cleaning storage units"}
wishlist-app | Prisma schema loaded from prisma/schema.prisma
wishlist-app | Datasource "db": SQLite database "prod.db" at "file:/usr/src/app/data/prod.db?connection_limit=1"
wishlist-app |
wishlist-app | SQLite database prod.db created at file:/usr/src/app/data/prod.db?connection_limit=1
wishlist-app |
wishlist-app | 26 migrations found in prisma/migrations
wishlist-app |
wishlist-app | Applying migration `20221229151102_init`
wishlist-app | Applying migration `20221229153655_signup_token`
wishlist-app | Applying migration `20221229183508_email_required`
wishlist-app | Applying migration `20230116175745_item_approval`
wishlist-app | Applying migration `20230201201336_lucia_key_0_5_0`
wishlist-app | Applying migration `20230203145115_system_config`
wishlist-app | Applying migration `20230221220353_add_item_purchased`
wishlist-app | Applying migration `20230306173006_add_key_expires_in`
wishlist-app | Applying migration `20230306211437_add_profile_picture`
wishlist-app | Applying migration `20230411190603_groups`
wishlist-app | Applying migration `20230411193602_lucia_1_0`
wishlist-app | Applying migration `20230811191732_lucia_v2`
wishlist-app | Applying migration `20231011193501_token_uuid`
wishlist-app | Applying migration `20231017150829_remove_expires_in`
wishlist-app | Applying migration `20231022025934_add_default_currency_symbols`
wishlist-app | Applying migration `20231128030637_add_claim_show_name_config_option`
wishlist-app | Applying migration `20240101035724_add_referential_actions`
wishlist-app | Applying migration `20240118052807_fix_initial_config_generation`
wishlist-app | Applying migration `20240330005940_lucia_v3_update_session_expires_at`
wishlist-app | Applying migration `20240330011054_lucia_v3_keys_removal`
wishlist-app | Applying migration `20240330024912_standardize_session_casing`
wishlist-app | Applying migration `20240330035425_add_indexes_and_standardize_db_naming`
wishlist-app | Applying migration `20240618010500_add_public_lists`
wishlist-app | Applying migration `20240625011332_add_system_user_for_anonymous_claims`
wishlist-app | Applying migration `20240825030505_add_display_order`
wishlist-app | Applying migration `20240902211258_add_price_id`
wishlist-app |
wishlist-app | The following migration(s) have been applied:
wishlist-app |
wishlist-app | migrations/
wishlist-app | └─ 20221229151102_init/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20221229153655_signup_token/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20221229183508_email_required/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230116175745_item_approval/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230201201336_lucia_key_0_5_0/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230203145115_system_config/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230221220353_add_item_purchased/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230306173006_add_key_expires_in/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230306211437_add_profile_picture/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230411190603_groups/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230411193602_lucia_1_0/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20230811191732_lucia_v2/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20231011193501_token_uuid/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20231017150829_remove_expires_in/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20231022025934_add_default_currency_symbols/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20231128030637_add_claim_show_name_config_option/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240101035724_add_referential_actions/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240118052807_fix_initial_config_generation/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240330005940_lucia_v3_update_session_expires_at/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240330011054_lucia_v3_keys_removal/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240330024912_standardize_session_casing/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240330035425_add_indexes_and_standardize_db_naming/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240618010500_add_public_lists/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240625011332_add_system_user_for_anonymous_claims/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240825030505_add_display_order/
wishlist-app | └─ migration.sql
wishlist-app | └─ 20240902211258_add_price_id/
wishlist-app | └─ migration.sql
wishlist-app |
wishlist-app | All migrations have been successfully applied.
wishlist-app | Running seed command `node prisma/seed.js` ...
wishlist-app | roles are synced
wishlist-app | created default group
wishlist-app | Patching item price model
wishlist-app | 0 items to update
wishlist-app | Finished patching item price model
wishlist-app |
wishlist-app | 🌱 The seed command has been executed.
wishlist-app |
wishlist-app | > wishlist@0.0.1 start /usr/src/app
wishlist-app | > node build
wishlist-app |
wishlist-app | Listening on 0.0.0.0:3000
However, it did not serve traffic
I double checked the compose file and it actually is serving up on 3280
I created a user with password, then moved to the next step
The SMTP was optional and not required, but I decided to try it as well as auto-approve on suggestions
The last step is to (optionally) invite users
Now i have a wish list
I can add a Wishlist item
I now have a nice listing for a very nice fish finder
Though, it would just be a private list for me at this point as the app requires login
I can easily signup, but it doesn’t let me see other users Wishlists
As admin in my primary account I can add a user
Once that happened, my second user could see my lists.
I tried the password reset
And soon I saw the reset email
Which did actually properly redirect
Kubernetes
Let’s start by converting this to a Kubernetes Manifest
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: uploads-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wishlist-app-deployment
spec:
replicas: 1
selector:
matchLabels:
app: wishlist-app
template:
metadata:
labels:
app: wishlist-app
spec:
containers:
- name: wishlist-app
image: ghcr.io/cmintey/wishlist
ports:
- containerPort: 3280
env:
- name: ORIGIN
value: "https://wishlist.freshbrewed.science"
- name: TOKEN_TIME
value: "72"
- name: DEFAULT_CURRENCY
value: "USD"
volumeMounts:
- mountPath: /usr/src/app/uploads
name: uploads
- mountPath: /usr/src/app/data
name: data
volumes:
- name: uploads
persistentVolumeClaim:
claimName: uploads-pvc
- name: data
persistentVolumeClaim:
claimName: data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: wishlist-app-service
spec:
selector:
app: wishlist-app
ports:
- protocol: TCP
port: 3280
targetPort: 3280
Then apply
$ kubectl apply -f ./wishlist.yaml
persistentvolumeclaim/uploads-pvc created
persistentvolumeclaim/data-pvc created
deployment.apps/wishlist-app-deployment created
service/wishlist-app-service created
Now I’ll create the ingress, but first I need an A record
$ cat ./r53-wishlist.json
{
"Comment": "CREATE wishlist fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "wishlist.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-wishlist.json
{
"ChangeInfo": {
"Id": "/change/C09827583PKJGXTOTSTJN",
"Status": "PENDING",
"SubmittedAt": "2024-11-15T02:49:38.250000+00:00",
"Comment": "CREATE wishlist fb.s A record "
}
}
Now an ingress to use it
$ cat wishlist.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
name: wishlist
spec:
rules:
- host: wishlist.freshbrewed.science
http:
paths:
- backend:
service:
name: wishlist-app-service
port:
number: 3280
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- wishlist.freshbrewed.science
secretName: wishlist-tls
$ kubectl apply -f ./wishlist.ingress.yaml
ingress.networking.k8s.io/wishlist created
Once the cert came up
$ kubectl get cert wishlist-tls
NAME READY SECRET AGE
wishlist-tls True wishlist-tls 2m20s
I can now start the setup
I’ll create an account
Once my account is setup, I’ll add an icon
Let’s try using it
Christmas Community
I can test it
builder@LuiGi:~/Workspaces/christmas-community$ docker compose up
[+] Running 1/1
✔ Container christmas-community-christmas-community-1 R... 0.2s
Attaching to christmas-community-1
christmas-community-1 |
christmas-community-1 | > christmas-community@1.36.0 start
christmas-community-1 | > node built/manager.js
christmas-community-1 |
christmas-community-1 | [ EXPRESS ] Express server started on port 80!
christmas-community-1 | [ DB EXPOSE ] DB has been exposed on port 8080
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /setup
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /libraries/bulmaswatch/default/bulmaswatch.min.css
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /libraries/fontawesome/css/all.css
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /libraries/animate.min.css
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /js/nav.js
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /css/main.css
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /img/logo.transparent.png
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /libraries/fontawesome/webfonts/fa-solid-900.woff2
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /img/logo.transparent.png
christmas-community-1 | [ EXPRESS ] ::ffff:172.23.0.1 - GET /manifest.json
I can test the login
and saw a landing page
I can add the same Garmin
but no icon on listing, just a new entry
Let’s convert this to a Kubernetes Manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: christmas-community-deployment
spec:
replicas: 1
selector:
matchLabels:
app: christmas-community
template:
metadata:
labels:
app: christmas-community
spec:
containers:
- name: christmas-community
image: wingysam/christmas-community
env:
- name: NODE_ENV
value: production
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: christmas-community-service
spec:
selector:
app: christmas-community
ports:
- protocol: TCP
port: 3000
targetPort: 80
Let’s apply
$ kubectl apply -f ./christmas.yaml
deployment.apps/christmas-community-deployment created
service/christmas-community-service created
Let’s create an A Record in AWS
$ cat r53-christmas.json
{
"Comment": "CREATE christmas fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "christmas.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-christmas.json
{
"ChangeInfo": {
"Id": "/change/C0045276KG6394M6AO7S",
"Status": "PENDING",
"SubmittedAt": "2024-11-15T02:13:39.490000+00:00",
"Comment": "CREATE christmas fb.s A record "
}
}
Now let’s create an ingress
$ cat christmas.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
name: christmas
spec:
rules:
- host: christmas.freshbrewed.science
http:
paths:
- backend:
service:
name: christmas-community-service
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- christmas.freshbrewed.science
secretName: christmas-tls
$ kubectl apply -f ./christmas.ingress.yaml
ingress.networking.k8s.io/christmas created
I then can connect and setup an Admin user
Once I created a user
I then dded a watch
Let’s try using it
SaaS offerings
I figured it was worth comparing to some simple hosted options we can easily find
Happy Wishlist
The first result I found was HappyWishlist
I pick a landing URL
Then can login with Google/Apple or create a fresh account
The MFA suggested the real backend is ‘throne.me’, I might add.
I next pick a country
And lastly set a public name and icon
Now we can create a list
GiftList
Another result that came up and even automatically prompted for federated login was giftlist.com
I did the sign-up with Google
Note: this MFA showed it was a Firebase hosted app (meaning GCP)
Once I verified age, I could start to make lists
And using it
More
There are so many more SaaS sites, and they are all very similar
And let’s not forget we can always use Target or Amazon to track our lists
Or Target
Summary
Today we explored two easy-to-use Gift list apps that are self-hosted, wishlist and the older but still very good Christmas Community. We fully set them up in Kubernetes and compared features before rolling to looking at some easy-to-use SaaS offerings. In fact, I just touched on the wide range of Gift Registry sites who all look to make a bit off referral links. Lastly, I just did a quick look at Amazon and Target.
I think the only issue I have with a mega-retailer like Target or Amazon is you get rather locked in on their offerings. This is fine for mainstream items, but what if I want a particular board game and they don’t list it
Or the Amazon price is a bit high compared to other retailers like Cabela’s (BassProShops)
Lastly, I think I just sort of soured on the concept; perhaps that is just me and my age. I don’t really want anything from others. I don’t mind a list for myself for those occasional out of the blue tax returns or bonuses, but I would feel rather weird someone dropped 3G on a trolling motor for me - so why list it?
I found my Amazon wishlist is an interesting stream of historical desires I had. It does show the passions of the seasons I have had over time (with the general theme of eclectic music, camping and fishing - and now boating.. for fishing).
In the end, I might suggest a simple hosted option would fit most peoples needs - and assume it could go away in time. If I had to think of long term solutions with family I might consider a shared Google Doc or just a backend blog entry. Perhaps the next blog on this should just focus on lists in general. Let me know what you think.