td204

Web development, one post per year since 2002

2018 in review: GDPR, and two native apps in three months

Two unrelated halves. The first was May 25th: every client, at once, in April, and a spring spent on consent, data minimisation and a command that scrubs personal data out of any database copy I pull to my machine. The second was the summer, when I wrote a native Android app in Java and a native iOS app in Swift, having never shipped either before.

The regulation itself is not the interesting part. What was interesting is that it forced me to answer a question I had been avoiding: which columns in this database are actually personal, and why is a copy of all of them sitting on my laptop?

Anonymising a production copy

If you work with a copy of a production database locally, everything privacy-sensitive in it has to go. I now ship an artisan command with the project that does exactly that. It always needs manual work, because only you can decide which columns count.

Take a plain User model. After pulling a copy you have every real customer, with firstname, lastname, email, password.

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class CleanupPersonalData extends Command
{
    protected $signature = 'cleanup:gdpr {aggressive?} {force?}';

    protected $description = 'Cleanup personal data for local environments';

    public function handle()
    {
        if ('production' == config('app.env')) {
            abort(403, 'This command should only be run in local/testing environments!');
            exit;
        }

        // overwrite everything, or skip what was anonymised before?
        $force = 'true' == $this->argument('force');

        // prefix, so we can recognise anonymised rows later
        $anonymizeString = 'anonymized: ';

        $faker = \Faker\Factory::create('nl_NL');

        // a column to check for the prefix
        $mainColumn = 'lastname';

        // include soft-deleted rows, they hold personal data too
        $items = User::withTrashed();

        if (!$force) {
            $items->where(function ($query) use ($mainColumn, $anonymizeString) {
                $query->where($mainColumn, 'NOT LIKE', DB::raw($anonymizeString) . '%');
                $query->orWhereNull($mainColumn);
            });
        }

        $items = $items->get();
        $this->output->writeln(sprintf('%s users', $items->count()));

        // hash once, bcrypt is expensive
        $pass = bcrypt('123456');

        foreach ($items as $item) {
            if ($force || strpos($item->$mainColumn, $anonymizeString) === false) {
                $item->$mainColumn = $anonymizeString . $faker->lastName;
                $item->firstname = $faker->firstName;
                $item->email = $faker->safeEmail; // never mail a real address by accident
                $item->password = $pass;
                $item->save();
            }
        }
    }
}

Repeat the block per model, run php artisan cleanup:gdpr after every import, delete the original dump. Three details that matter more than the code:

  • withTrashed(). Soft-deleted rows are still personal data. Skipping them is the mistake I made first.
  • safeEmail. A test run that mails two thousand real customers is a very bad afternoon.
  • The environment guard on line one. Not because you would ever run it on production deliberately.

Two apps, two languages I do not write

Between July and October I built the same product twice: Android in Java with Gradle, then iOS in Swift with CocoaPods. Not a webview wrapper around a website, which is what I would have quoted a year earlier. Actual native apps, twice, because in 2018 that was still what the two platforms required if you wanted it to feel right.

What I expected to be hard was the languages. It was not. Java and Swift are both perfectly reasonable, and after fifteen years of PHP the type systems were a relief more than an obstacle.

What was actually hard was everything around the code:

The build is the platform's, not yours. Gradle and CocoaPods are not npm. Signing keys, provisioning profiles, entitlements, target SDK versions. On the web I control the whole chain. On mobile I am a guest, and the host has opinions about certificates.

There is no deploying a fix. I had spent my whole career being able to correct a mistake in minutes. Suddenly there is a review queue between me and my users, and a copy of last week's bug sitting on every device that has not updated. It changes what "done" means, and it made me test in a way that web work never forced me to.

Doing it twice is doing it twice. Same screens, same logic, two codebases, and a fix in one is not a fix in the other. The 2008 lesson about copying a folder nine times, wearing a different hat.

I have not written a mobile app since. But three months of not being able to hotfix is the best argument for testing I have ever been given, and I brought it back to the web work with me.

The consent side

The cookie module was easier to build than to specify. The hard question was never technical: it was getting a client to say out loud which third-party scripts they actually needed. Most of the time the honest answer turned out to be "fewer than are currently on the site", and a couple of tags got deleted rather than consented.

In short

GDPR took the spring: a consent module and an artisan command that anonymises any database copy on import. The summer went to a native Android app in Java and a native iOS app in Swift.

The year in commits

Commits by month in 2018: 1265 in total, peaking at 210 in Jan.
Jan 2018: 210 commits 210 J Feb 2018: 49 commits F Mar 2018: 19 commits M Apr 2018: 72 commits A May 2018: 102 commits M Jun 2018: 58 commits J Jul 2018: 137 commits J Aug 2018: 135 commits A Sep 2018: 94 commits S Oct 2018: 166 commits O Nov 2018: 101 commits N Dec 2018: 122 commits D