My wife was interested in tarot cards, and I was a frontend developer looking for an excuse to build something. I put together a page with a deck, a card-flipping animation, and an AI-generated reading.
With AI assistance, the basic flow worked in about two hours.
I thought authentication and payments would get it ready to launch. Then the questions started: What happens when someone switches devices? What if a paid reading stops halfway through? Can I change a guided exercise without changing the meaning of someone’s saved answers?
Eventually, another question became harder than any of those: how do I get people to use it?
The project is now Mystic Journey. It includes a daily card ritual, guided explorations, a personal journal, and an optional companion board.
I’m still looking for early users. This is a development retrospective—and, openly, an invitation to try what I’ve built.
Why I changed the product
The original experience was straightforward: ask a question, draw cards, read the interpretation, leave.
But a general-purpose AI can already explain tarot cards. A nicer animation wasn’t enough to explain why someone should return to my website.
I started thinking about the experience around the reading. Writing in an empty journal can be surprisingly difficult. A card, a specific question, or a few choices might make it easier to begin reflecting.
That became the daily ritual: choose your mood, reveal your daily card, read a reflection prompt, and optionally write a few words. The aim is to make a small check-in easy, even when you don’t have a major question to ask.
For people who want to go further, I added six guided themes covering space, boundaries, change, confidence, connection, and direction. Each has five short chapters: notice the situation, consider another perspective, choose a small action, identify obstacles, and find support.
These explorations use curated branches rather than generating every step with AI. Earlier choices influence later options. Personal notes are optional and don’t automatically trigger a model call.
This gives me content I can review and improve consistently. Open-ended AI readings still have a place, but they don’t need to power every interaction.
Completed rituals, explorations, and explicitly saved readings go into a personal Journey timeline. The intention is to give people something worth revisiting: their own thoughts and choices, not just a list of cards.
I also added an opt-in weekly companion board. It shows names, avatars, and activity points, but not private reflections. Points are separate from purchases and don’t depend on someone’s mood or how much they write.
There’s a tension here: even a gentle leaderboard can create comparison. Whether it feels supportive is something I need users to tell me. These are product hypotheses, not proven retention improvements.
A small stack, with more state than I expected
The stack is Nuxt 3 for the statically generated frontend, a separate Fastify API, SQLite, and DeepSeek for generated readings. Nginx routes API requests to the backend.
For the current scale, I want something I can deploy and troubleshoot on my own. The difficult work has mostly been in the behavior of the application, not the number of services.
A daily card becomes account data once it connects to reflections, history, and rewards. Someone opening the site on their phone should see the same record they started on their laptop. Completion and rewards need consistent server-side state.
Disabling a button helps the interface, but it doesn’t prevent retries or requests from another tab.
Guided exploration saves have a similar problem. If a laptop has already saved the next chapter while a phone still has an older revision, accepting the phone’s write could overwrite newer progress.
Relevant save requests carry a revision. A stale write gets a conflict response, and the frontend keeps the draft while offering to reload saved progress. Identical committed retries return the existing result.
These are easy details to overlook when you only test one browser tab on a reliable connection.
The content needed versioning, too
One of the more interesting problems came from changing the exploration itself.
Suppose a saved answer says the user selected option 1. If I reorder the options next week, that stored value can appear to mean something different.
This is a simplified example of the problem:
Version 1, option 1: Take time to reflect privately
Version 2, option 1: Talk to someone you trust
The number didn’t change. The user’s apparent answer did.
An exploration therefore retains the content version it started with. Old option meanings need to remain stable, and the live chapter and saved recap need to interpret answers through the same branch logic.
Once copy gives meaning to stored answers, editing it is also a data-compatibility decision.
Weekly reflections brought another state problem. A generation request can time out, a retry can start, and the original request can finish late. Without a guard, that older result could overwrite the newer one.
The implementation uses a generation state, an expiring lease, and a version condition when writing the result. A short database transaction protects state changes; it doesn’t stay open while waiting for the model.
A paid AI request has more than two outcomes
The demo treated generation as success or failure. Streaming makes that less tidy: a request can return some text, stall, lose its client connection, or finish without usable content.
Deep readings use server-sent events. The server applies a timeout and attempts to cancel the upstream request if the client disconnects early. Only a complete successful result goes into the result cache.
If generation fails after credits have been deducted, the error path restores those in-app credits and provides a local card interpretation. Restoring credits is separate from refunding a payment through the payment provider.
Weekly reflections also have a model-call budget and a local fallback. A free feature still needs a cost boundary.
This isn’t a complete recovery system. For example, a process crash after a deduction cannot be recovered by a catch block in that same process. Durable job state and reconciliation remain areas to improve.
That distinction matters: having an error handler is not the same as having verified recovery from every failure.
Security extends beyond the prompt
The model generates text. It doesn’t decide account permissions, payment status, or credit balances, and it has no database or payment tools.
That limits the consequences of a manipulated response, but it doesn’t solve prompt injection. User input can still interfere with the intended task, and model output needs to be treated as untrusted content when it reaches the UI.
The service currently has request rate limiting, body-size limits, and account and credit checks before paid generation. These are basic controls, not a claim of comprehensive protection. IP-based limits have limitations, and input handling and output rendering require their own review.
For accounts, passwords use salted hashes, the database stores hashes of session tokens, and session cookies use HttpOnly and SameSite settings, with Secure enabled for production. Verification codes expire and have attempt limits.
Payments have a separate trust boundary. The backend confirms payment status with the provider and verifies webhook signatures. Order-status requests check ownership. Since polling and a webhook can both confirm the same payment, crediting an order must be idempotent.
Privacy also affects ordinary feature decisions. The feedback form doesn’t automatically attach someone’s journal entries. Analytics can record that a reflection was saved without collecting its text.
I still have security work to do. Keeping track of what each component can access—and where personal content goes—has been more useful than treating safety as a few lines in a system prompt.
Performance and localization added their own work
Static generation lets me serve public pages separately from account APIs, but it doesn’t automatically make the experience fast. Images, animations, authentication checks, and API latency still matter on a phone.
The Journey timeline is paginated, details load on demand, and uploaded avatars are cropped and compressed to WebP in the browser. Streaming also needs the proxy to cooperate: response buffering can hide the incremental output the server is sending.
The interface supports eight languages, while the card data is primarily English and Chinese, with English fallback elsewhere. Those are different levels of localization, and I shouldn’t present them as equivalent.
A recent bug was a good reminder of the extra surface area: an email placeholder contained a literal @, which the i18n message compiler interpreted as special syntax. The JSON was valid, but the page failed. A JSON parse check alone couldn’t catch it.
Even a small copy change needs the right kind of validation.
Shipping didn’t bring users automatically
I’ve shared the product on Solo, 出海栈, 小众软件, and Indie Hackers. That has given it places to be discovered, but I don’t yet have enough evidence to call any channel a repeatable source of users.
I’m also learning to separate people who enjoy a development story from people who want the product. Developers may find concurrency handling interesting. A potential user wants to know what they can do, how much effort it takes, and whether their writing stays private.
There’s overlap, but a well-read technical post doesn’t prove demand.
My next experiments are technical retrospectives like this one, short product demonstrations, and content built around concrete reflection scenarios. Search is another ongoing task: a readable sitemap and a renderable page are necessary pieces, but I still need public content that answers something people actually look for.
What I want to measure is the path after a visit: does someone complete a first ritual, continue to another exploration chapter, or return a few days later? Knowing where that stops should help me choose between improving the entry flow, the content, or the acquisition channel.
Writing more code is the comfortable option. It gives me a visible result. Asking someone to try the product can end with “I’ll take a look” and nothing else. I’m trying not to mistake the comfort of development for evidence that another feature is needed.
Where the project is now
Mystic Journey is still early. I’m looking for a small group of people willing to try it and tell me what feels useful, confusing, or unnecessary.
The daily ritual and guided explorations are free; deeper AI readings use credits. If tarot is unfamiliar, you can start with an exploration theme that fits something you’re thinking about.
Specific feedback would help most: where you got stuck, what didn’t make sense, or where you stopped wanting to continue. There’s a feedback form in the app, and comments here are welcome too.
If you’ve taken a side project beyond the demo stage, I’d also like to hear how you found your first users who came back.















