Triple-H: Hackers, Hustle, and Hints of Revenue

We're not quite two weeks into the new year and I've been busy tackling that dreaded marketing and promotion process for each of the apps. In addition, I've added new features to AudiBar, updated multiple websites and dealt with visitors from Hong Kong attempting to tamper with server requests via JavaScript. I'll go into the security issues before ending this post. Until then, let's get started with the marketing!


AudiBar Marketing

I started marketing AudiBar on the 5th January, purely because it was the first app that was released on the App Store. I wanted to go through the process and learn from it before applying the same process with some experience under my belt for the others. The first three days were spent figuring things out. Here's how it went.

  • Posted on the socials: Bluesky, X/Twitter, LinkedIn, Mastodon, and Pinkary
  • Sent an email to 9to5Mac: Indie app spotlight.
  • Localised app store metadata in App Store Connect
  • Added in-app reviews & rating to the app
  • Added cross-site app promotion to the website

Signed up and submitted the app to:

Reddit

A big part of the marketing strategy was related to Reddit. While I've read posts from Reddit over the years, I've never had an account or had the need to participate in the comments section of them. In hindsight, I should have got involved years ago. I can't promote anything on Reddit at the moment as my account is new and I have literally one karma point.

Throughout January and the coming months, I'm commenting on Reddit posts where I have something to say and can offer something of value. I'm certainly not commenting on anything for the sake of karma points (karma farming?). It'll take a while, but I've made a start. For now, Reddit promotion will have to wait until a later time.

App Store Metadata

I took the time to set all the app store metadata for these base languages:

  • English (American, British & Australian)
  • Spanish (Spain & Mexico)
  • Portuguese (Portugal & Brazil)
  • French
  • German
  • Korean
  • Japanese

As I had added the metadata for these languages, the app needed another review. This would be a good time to make any minor improvements to the app, so before submitting for review, I decided to take a few hours to add the following:

In-App Review and Rating

I set this to only start showing seven days after the first time installation. Then it shows three times in a six month period from then on, then not at all after that. It's also available in the context menu of the app's menu bar, so it's always available.

Custom Themes

I had the idea of creating different themes. Then I figured why bother creating themes when I can just let users choose their own primary and secondary colours. The app uses gradients (hence two colours). This literally took under 30 minutes to implement and works great. Now users can make the app their own.

Purchase Restore

I noticed the app upgrade page didn't have a restore purchase option. I'm surprised the review team accepted the app without one. Adding one took five minutes, and while the app checks purchases before showing the upgrade window, Apple requires one to be available. Now it has one.


AudiBar Website Updates

Since I had added localisation to the App Store in the various languages, it made sense to make the website available in those languages as well which would mean taking all the hard coded text from the files and creating localisation files for each language. I set up Claude to do this job, which it managed to get done with no issues in just under 20 minutes.

I added a middleware to set the language on each request, a language switcher at the top of the page, and a banner that detects the user's country and suggests switching. You know the one: "Site is available in Korean – Switch to Korean version" type thing. Long way of saying the AudiBar website is now fully translated in the same languages used in the App Store.

Long Words and Layout Issues

Making your website available in multiple languages often messes with layouts. French text typically runs 15–20% longer than English. German is even worse, often expanding by 25–35% thanks to those compound words. I had to make some layout and text size adjustments for certain languages.

Hmmmm. As the site uses Tailwind 4 CSS, I had to figure out how to add Tailwind classes only when the language was French or German. Well, it turns out this is much simpler than I thought. For anyone reading this who needs to do similar, here is how it can be done.

In your app.css file (I'm using Laravel, but yours might be different), simply add:

@import 'tailwindcss';
@custom-variant fr ([lang="fr"] &);

If you need to target multiple languages:

@import 'tailwindcss';
@custom-variant fr ([lang="fr"] &);
@custom-variant de ([lang="de"] &);

Or if applying the same style to multiple languages:

@import 'tailwindcss';
@custom-variant intl ([lang="fr"] &, [lang="es"] &, [lang="de"] &);

How do you use this variant? Simply by adding a Tailwind class as:

<div class="bg-blue-500 intl:bg-green-600">Content</div>

It really is as simple as that.

Extra Visibility

While making changes for localisation, I added a sitemap route for Google, so it knows about all the versions available. Speaking of search engines, I also added a site schema for them just to help out visibility a little more.

Cross-Site Promoting

Finally, I added cross-site promotions. By this I mean a section at the bottom of all pages which randomly shows three to four other apps available by me with an image, title, description and a button link to each app's website. It worked so well, I updated all the other websites with the same cross-site promotion.

Since starting the marketing for AudiBar and making improvements to the app's website, I've had some traction resulting in more downloads and some upgrade purchases! Happy days! We're still a long way from millionaire territory, but it's a start!

Customer Feature Request

Before I end off talking about AudiBar, a few days ago I had a customer email asking me to consider volume controls for the app as the app uses the system volume controlled from there. After asking the customer for a use case for this, I spent an hour adding this as an option.

Premium users now have a setting to enable in-app volume controls. Once enabled you can control AudiBar's volume separately from your Mac's system volume. Perfect for keeping background music at a comfortable level while your notifications and other apps stay at your preferred volume. This simply uses the system's volume as a max volume. The app volume slider just sets the app's volume as a percentage of the system's max volume.


Kanodo: Finally Released!

If you followed the rejection saga from my last post, you'll be relieved to know it's finally over. It took the App Store review team a week to get back to me on the issues they were reporting. In the end I bit the bullet and edited the preview videos to remove the intro titles. Resubmitted. A few days later, Kanodo was finally approved (7th January). Finally!


Chronode: Good News and Some Bad

Early last week, Chronode got its first sale. Bear in mind that Chronode is a self-published app, not available through the App Store, and relies on search engine visibility for any traffic. The fact that it got a sale made all the effort worthwhile. Great news!

But then the customer emailed explaining that the app wasn't working as expected. Damn. The customer had gone to lunch and when he came back, Chronode stopped tracking anything. I managed to identify and fix the problem within an hour.

Docker Issues

A few hours go by and I'm feeling confident, when I get a response. Docker wasn't being tracked properly when opened. Also focus mode was not working properly either. I had to spend the afternoon on a deep dive to figure out what was going on.

It turns out Docker Desktop uses a different internal identifier for its UI window. So when you add Docker as an app to track, it saves the bundle identifier, so it knows what app to look out for. I suspect Docker Desktop is an Electron app and so that could explain why the bundle and internal identifiers were different.

Focus Mode: Reworked

I reworked how focus mode works within Chronode to ensure it resumed after waking from sleep, currently works even when toggling focus mode on and off mid-session, and tracks all apps properly regardless of project detection.

Refresh rates

While working on the fixes, I noticed that the sidebar wasn't updating very often. I realised this was due to the refresh rate having been set to refresh every 60 seconds. This is too long, so I changed this to every five seconds but ensure the sidebar only refreshes if the app window is open. I also ensured that the refresh timer gets paused when the mouse is over the sidebar, otherwise the menu controls of each app and project being tracked are inaccessible.

I've not heard back from the customer, but I'm hopeful that these fixes resolved the issues he experienced.


Security Issues

As I mentioned at the start, I've had visitors from Hong Kong attempting to tamper with server requests via JavaScript. Allow me to explain.

I'm primarily a Laravel/PHP developer and so all my sites use Laravel, or what is commonly called the TALL Stack (Tailwind, Alpine, Laravel, and Livewire). Livewire uses AJAX requests for communication between the page and the server. One of the great things about using the TALL Stack is that I can use PHP variables with JavaScript interchangeably. As an example:

<?php

namespace App\Livewire;

use Livewire\Component;

class Page extends Component
{
	public ?int $number = null;
}

In Blade, I can use $number as a PHP variable {{ $number ?? 0 }} and also via JavaScript $wire.number;

Unfortunately as the number variable is a public property, it can be updated using JavaScript in the browser console like so:

Livewire.find('goXLALPOU1w3aMvfgtS6').set('number', 800);

Lock it up!

One thing we can do to prevent public properties from being tampered with is to add the #[Locked] attribute to it:

<?php

namespace App\Livewire;

use Livewire\Attributes\Locked;
use Livewire\Component;

class Page extends Component
{
	#[Locked]
	public ?int $number = null;
}

Now when anyone tries to change it, it will result in a server error throwing an exception that locked properties cannot be changed that way. If we're not able to set it as a locked property (e.g. form fields), then this is where type hinting is extremely important. Lets say our LIvewire component had a non type-hinted property set:

<?php

namespace App\Livewire;

use Livewire\Component;

class Page extends Component
{
	public $number = null;
}

Anyone can set the number property to anything (string, array, int, float). I've had a few instances lately where a user has tried to tamper with a variable by attempting to set it to an array. As an Example:

<?php

namespace App\Livewire;

use Livewire\Component;

class Page extends Component
{
	public ?string $email = null;
}

When attempting to set the property to an array using JavaScript via the console, an exception is thrown.

Livewire.find('goXLALPOU1w3aMvfgtS6').set('email', []);

As I had $email type hinted to a nullable string, when the user tried to set it to an array, it threw an exception that the server could not set a string to an array. Without that, I'm not sure what damage could have been done. There are a few lessons to be learned here when using Livewire:

  1. Always type hint your properties. This should already be done as standard in any programming language, but there it is.
  2. Lock properties where possible and update them via method calls as needed.
  3. Form field properties should be validated anyway and/or use #[Validate] attributes.

Exception Emails

I have all my websites set up with exception emails, so if anything goes wrong on any of the sites, I get an email telling me what went wrong and why. Unfortunately this meant I was getting bombarded with emails when our Hong Kong visitors were trying to tamper with something. I had to add exclusion checks to stop the mailer sending these. I'd rather not do this, but to save my inbox from the onslaught, it had to be done.

Geo Blocking

One step I have taken for all the websites is geo blocking. This is often hit and miss and yes of course there are always workarounds like VPNs. That said, for 30 minutes' work to implement geo blocking, it was worthwhile. Using the MaxMind geo library, I've added location checks and blocks for certain countries. Like I said, it's not bulletproof, but it's something.

I tried implementing something similar at server level, but there was some dependency issue between packages available for something installed on my server. So rather than fanny about trying to make it work with a great deal of ball-ache, I opted for application/site level geo checks instead.


Exoden: Early Beginnings

Alongside the marketing push, I've been chipping away at the Exoden web app using Laravel and Livewire. So far I've tackled authentication, Stripe integration, and account management, writing user guides and unit tests as I go.

I could have used one of Laravel's starter kits, but I wanted a passwordless approach. Users receive an email with both a magic link and an auth code, so they can either click to log in or enter the code manually. If they've enabled 2FA, they'll need their authenticator app after that. And if they've set up passkeys, they can skip the email step entirely. It's the kind of flexible, secure flow I'd want to use myself.

Stripe subscription handling is done via Laravel's Cashier package, with Reverb powering real-time notifications.

With January dedicated to marketing, I can only grab time for Exoden here and there. That said, the core foundation is pretty much complete. Now it's time to start building what the app is actually for: invertebrate husbandry and collection management. No sign-ups from the landing page yet, but then I've done zero promotion. That'll come later. I'm targeting a launch around mid to late Q2, so May or June time.


Wrapping Up

There we have it. Just about 12 days into the new year. Some things are going well, others not so well, but then I don't expect constant success with no issues or niggles. It's all a learning curve, in both programming, app development and marketing. The last 12 days have brought in nearly three times what the entire last four months of 2025 did. We're not talking life-changing money yet, but it's proof that marketing actually works.

I'm off to start the marketing process for Kanodo. Let's see how this one goes.


Comments (0)

Optional. If given, is displayed publicly.

Optional. If given, is not displayed anywhere.

Required with 25 characters minimum

Comments are reviewed for spam and may require approval.

No comments yet. Be the first to share your thoughts!

Stay in the Loop

Occasional updates on new articles and projects. No spam guaranteed