12/06/2020

How to upload Shopify PDF invoices to Google Drive with Zapier

Yesterday I discovered that my fiancée was manually downloading every invoice (generated with Order Printer Pro app) from Shopify so then she could upload them to her Google Drive "invoices" folder. It's more than a hundred manual operations per month and that number is growing. FAST.

Let's automate that with Zapier 😇.

Step 1: Trigger Zap on new Shopify paid order

First thing first, create a new zap with a Shopify trigger "New Paid Order".

Step 2: Add some glue code

Sadly the Shopify connector does not expose metadata from apps like Order Printer Pro. We won't get the pdf invoice link for free. Hopefully we can download the receipt page html source code and extract the "Download Invoice" link from there.

Add a "Code by Zapier" step along in order to run JavaScript code. Setup the 3 inputs variables below:

Copy/paste the -good-enough- code below:

const order_name_as_file_name = inputData.order_name.replace('#', '-');

fetch(inputData.order_url)
.then((res) => res.text())
.then((body) => {
  const url_regexp = /href="(.*?)"/ig;
  let match;
  while(match = url_regexp.exec(body)){
    if(match[1].includes('.pdf')){
      return callback(null, {
        order_name: order_name_as_file_name, 
        invoice_url: match[1].replace('/.pdf', `/${inputData.order_number}.pdf`)
      });
    }
  }
  
  // we could not find the PDF invoice, fail loudly
 callback(`Could not find pdf URL in: ${body}`);
})
.catch((err) => callback(err));

Step 3: Upload PDF Invoice to Google Drive

One last thing, add a "Upload File in Google Drive" action, select your drive and folder. Select the "Invoice URL" we got from our previous step as the file parameter, customize the filename as needed and you are good to go!

Job done, you now get full tracability and alerting capabilities from Zapier, enjoy!

11/20/2020

How to fix "there is no timer running, must be called from the context of Tokio runtime"

Some context

You just tried to compile your Rust project and got the there is no timer running, must be called from the context of Tokio runtime error?

You are desesperatly trying to understand what is going on?

So did I!

For future reference, my project relied on tokio v0.3. I added a dependency (bollard) that was relying on tokio v0.2.

Even when my code was in the execution context of a tokio runtime (v0.3) calling bollard code triggered at runtime the there is no timer running, must be called from the context of Tokio runtime error.

How do I fix this?

Option 1: start a new runtime in tokio v0.2

Your first option would be to start another runtime in the same tokio version that your dependency requires, something like:

use tokio::runtime::Runtime;

// Create the runtime
let rt = Runtime::new().unwrap();

// Spawn a future onto the runtime
rt.block_on(async {
    // call your dependency
});

That would result in two threadpools so it might be an issue in some case. I did not go down this path

Option 2: use the same version that your dependency

It's the easiest option. Downgrade your tokio project dependency to v0.2 so it can be the same as your dependency. But yep, it sucks.

Option 3: send a patch to the dependency

Last option (and the best one) is to create and send a patch to your dependency project to upgrade its tokio version.

11/02/2020

How PostgreSQL triggers works when called with a PostgREST PATCH HTTP request

Wonder what values are set or not inside your new.column_name and old.column_name when you are calling PostgREST with a PATCH request? This article is for you!

Create a private schema for our sample app, we will expose the public schema for our PostgREST API:

create schema private;

A city table:

create table private.city (
    city__id integer not null primary key ,
    name text not null,
    countrycode character(3) not null,
    district text not null,
    population integer not null
);

with some data:

copy private.city (city__id, name, countrycode, district, population) FROM stdin;
1	Kabul	AFG	Kabol	1780000
2	Qandahar	AFG	Qandahar	237500
3	Herat	AFG	Herat	186800
\.

Now let's expose this city private table through PostgREST as a public API (public schema) so we stay clean regarding the Separation of Concerns principle:

create view public.cities as 
	select city__id as id, name, countrycode, district, population 
    from private.city;

Let's add support for the PATCH HTTP verb on our newly created /cities REST endpoint.

create or replace function private.update_city() returns trigger as
$$
begin
    raise exception 'new.name = %, old.name=%, new.countrycode = %, old.countrycode = %', new.name, old.name, new.countrycode, old.countrycode;

    return new;
end;
$$ security definer language plpgsql;


create trigger city_update
    instead of update
    on public.cities
    for each row
execute procedure private.update_city();

Now our function private.update_city() will be called for each rows submitted through PATCH /cities HTTP request. As you can see from update_city function body we print the before/after values of name and countrycode columns in PATCH requests.

What's the value of new.countrycode if I don't specify countrycode property in PATCH request body?

# PATCH /cities?id=eq.1 '{"name": "new_value"}'
curl -H "content-type: application/json" \
	--request PATCH \
    --data '{"name": "new_value"}' http://localhost:3000/cities?id=eq.1 | jq '.message'
new.name = new_value, old.name=Kabul,
new.countrycode = AFG, old.countrycode = AFG
As you can see, the property (column in fact) countrycode was not specified in the PATCH request body but it still defined with its current value in new.countrycode and old.countrycode just like we expected it to be.

What's the value of new.name if I set name as a null value in PATCH request body?

# PATCH /cities?id=eq.1 '{"name": null}'

curl -H "content-type: application/json" \
	--request PATCH \
	--data '{"name": null}' http://localhost:3000/cities?id=eq.1 | jq '.message'
new.name = <NULL>, old.name=Kabul,
new.countrycode = AFG, old.countrycode = AFG

Perfect! Just like we expected. We set name property to null in our PATCH request body thus new.name is set to NULL in our trigger function.

Clone this github repository to try all of this locally. Wonder what SQL conventions you should use? Check out these SQL conventions.

9/01/2020

🤝 14 étapes pour vendre son SaaS en 3 mois (et pas 2 ans)

J'ai fait l'erreur il y a 2 ans de penser que la vente de mon SaaS, Redsmin.com (je parle de son histoire ici), serait naturelle. Malgré les demandes reçues au fil du temps, rien ne me convenait. Il y a 3 mois, j'ai décidé de me prendre en main ce qui a eu pour résultat une vente de Redsmin jeudi dernier (20 août 2020). Cet article retrace les étapes que j'ai suivies.

1 - Attendre un miracle

Cela faisait 2 ans (juillet 2018) que je laissais Redsmin en roue libre. Pas de mise à jour fonctionnelle, juste un peu de support à raison d'un email par semaine environ. Le MRR variant de mois en mois entre 1 700$ et 2 800$, cela me convenait et j'espérais recevoir des propositions intéressantes.

La bonne nouvelle est que j'ai bien reçu des propositions, la mauvaise nouvelle est qu'elles ne m'intéressaient pas. Offre de partenariats, prise de participation, AcquiHire...

Je n'étais pas proactif quant à la communication et la gestion de la mise en vente de Redsmin et le projet stagnait.

Bref, j'attendais un miracle, tout comme on peut attendre d'obtenir un travail en restant chez soi. Les chances de succès sont très limitées.

2 - Se prendre en main, publier l'annonce sur des marketplaces

Le 11 mai 2020, je décide de rechercher les techniques pour vendre son SaaS. Je réalise qu'il existe des marketplaces spécialisées pour cela.

Je crée une fiche profil pour Redsmin.com sur indiemaker.co, la fiche est validée le jour même par le site.

Deux semaines plus tard, le 22 mai, je découvre microacquire.com. Rebelote, création d'une fiche profil qui sera validée 4 jours plus tard par le site.

3 - Attendre les propositions

12 jours plus tard, j'ai pu recevoir la première demande de mise en contact via Indiemaker. Concernant MicroAcquire il a suffi de 5 jours d'attente.

Au total, c'est 28 prises de contact qui ont eu lieu (19 via MicroAcquire, 9 via IndieMaker). Ces contacts ont découvert la fiche du SaaS majoritairement grâce à la newsletter dédiée de ces deux sites ainsi qu'une mise en avant par les webmestres.

4 - Constituer un dossier de vente

Les potentiels acheteurs vont avoir besoin de plus que vos beaux yeux pour prendre la décision d'aller plus loin.

Il est donc nécessaire de créer et de partager un dossier qui contiendra un ensemble d'informations communicable à l'extérieur.

Je suis parti sur un dossier par mois. Chaque dossier contenant des exports de données relatives au mois ou aux 6 derniers mois, suivant la métrique.

Plutôt que de m'embêter à réaliser des exports partiels de la base de données et de Stripe. J'ai préféré utiliser les offres gratuites de ProfitWell et ChartMogul pour agréger et réaliser des exports.

J'obtenais ainsi très facilement les informations suivantes pour chaque mois :

  • MRR over the past 6 months
  • MRR per plans
  • MRR per countries
  • MRR movements
  • MRR breakdown (new business, expansion, contraction, churn, reactivation) over the past 6 months
  • ARR
  • Subscribers count over the past 6 months
  • Churn/retention cohorts
  • Cash-flow
  • Freemium: free/paid ratio
  • Customer Lifetime Value

À ceci il faut ajouter un export de votre comptabilité interne (profit & loss).

Ce dossier étant dans dropbox il ne me restait plus qu'à partager le lien, suite aux mises en relation.

Rétrospectivement, j'aurais dû d'abord constituer un dossier avant de soumettre les fiches. Même s'il ne faut pas plus de quelques heures pour créer une première version.

5 - Maintenir un document de Q&A

Les questions des potentiels acheteurs sont souvent les mêmes. Je ne sais pas vous, mais je n'aime pas trop me répéter. J'ai donc ajouté un fichier Q_and_A.txt qui répertorie l'intégralité des questions que j'avais pu recevoir ainsi que leurs réponses associées donc voici un extrait :

  • How much effort do you need to maintain this product?
  • What are the unique values of your SaaS product, compared to your competitors, like XXX ?
  • If you have time to make some dev effort, what functions will you add?
  • What does the overall tech stack of the prod look like?
  • It seems the maintenance cost is low, why do you still sell it?
  • What is included in the sell and more importantly what is not included?

Sur les 28 mise en relations, j'ai pu ainsi partager l'accès au dossier de vente à 10 d'entre eux.

6 - Filtrer les propositions

Suite au partage du dossier de vente, j'ai pu recevoir 5 propositions (conversion : 17%). 

Je considérais une proposition comme intéressante si elle respectait les critères suivants :

  • un maintien du service pour les clients et utilisateurs existants
  • une vente intégralement en cash
  • le montant correspondait à mes attentes

Pour ces raisons, j'ai donc rejeté les propositions suivantes :

  • acquihire (acquisition + recrutement pour continuer à maintenir Redsmin)
  • apport + entrée au capital de Redsmin
  • un peu de cash + une commission sur les nouvelles ventes
  • un peu de cash + un second versement en fonction des résultats après 1 an

Sur ces 5 propositions, en suivant mes critères, deux ce sont avérées intéressantes. Vu que le premier y allait presque au chantage, j'ai donc continué avec le second acheteur nous avons à ce moment là échangé sur le facteur multiplicateur.

7 - Méthode EBITDA et facteur multiplicateur

Pour estimer la valeur d'un SaaS plusieurs méthodes existent mais la plus connue et sans doute l'EBITDA (Earnings Before Interest, Taxes, Depreciation and Amortization). En France nous parlons d'EBE (Excédent Brut d'Exploitation). 

EBITDA = Chiffre d'affaires - Charges d'exploitation

Redsmin est un SaaS et son business model est un modèle de souscription majoritairement mensuel. Pour évaluer le chiffre d'affaires dans ces cas là, on se base sur un MRR médian (par exemple sur les 6 derniers mois) reporté sur 1 an.

Ensuite l'EBITDA est multiplié par le fameux facteur multiplicateur afin d'obtenir le montant final de la vente.

L'intérêt de la méthode EBITDA et facteur multiplicateur est qu'elle donne un repère à l'acheteur ainsi qu'au vendeur. En résumé : on ne peut pas fake son EBE/EBITDA (cette affirmation est à prendre avec des pincettes, je ne suis loin d'être un expert). 

Il ne reste plus alors qu'à jouer sur le facteur multiplicateur pour trouver un terrain d'entente.

Bref, connaissant le modeste MRR de Redsmin, je savais pertinemment que cette vente ne me rendrait pas millionnaire 😅.

8 - LOI - Letter Of Intent

La lettre d'intention d'achat (ou Letter Of Intent en anglais) a principalement deux intérêts :

  • une première définition du prix d'achat du SaaS
  • la définition d'une période (e.g. 14 jours) d'exclusivité où le vendeur s'engagent à ne pas accepter d'autres propositions

Néanmoins la LOI n'engage habituellement pas l'acheteur, il faut donc encore montrer patte blanche !

9 - Due Diligence

Pour résumer, l'objectif de cette étape (appelée Due Diligence) est de vérifier que le vendeur (moi) ne bullshit pas sur les chiffres. Dans notre cas la Due Diligence s'est déroulée via un échange Google Meet (l'acheteur était anglais et d'un autre pays) et un partage d'écran, en 1 heure c'était plié.

Au programme :

  • Parcours du code et explication macro via GitLab et GitHub. 
    • Objectif : démontrer que le code n'est pas dégueux, qu'il y a des tests, une intégration continue (CI) et parfois même du déploiement continu (CD), bref, que l'application est toujours maintenable.
  • Parcours des revenus via Stripe. 
    • Objectif : prouver que je ne bullshit pas sur les exports et sur les chiffres.
  • Parcours des données de visites via Google Analytics.

10 - Asset Purchase Agreement, Asset Transfer, Non Compete Agreement, NonDisclosure Agreement

À cette étape il faut définir un inventaire des assets à transférer (comptes Stripe, Analytics, Google Apps, GitLab, GitHub, npm, Tumblr, Netlify, OVH, Clever Cloud, MongoDB Atlas, RedisLabs, Uservoice, …), sous quelle modalité, avec quel pré-requis...

L'échange se concentre en parallèle sur le contrat d'Asset Purchase Agreement. Dans mon cas nous avons itéré 7 fois afin de bien spécifier les clauses légales.

En parallèle nous avons chacun ouvert un compte sur Escrow.com (oui, la compréhension VF du nom ne rassure pas !). Escrow est une plateforme agissant en tant que tiers de confiance pour assurer la transaction.

11 - Négocier l'accompagnement 

Nous sommes partis sur un accompagnement de 30 jours intégré au contrat de vente. Puis une facturation à l'heure pour du conseil après ces 30 jours. 

Rétrospectivement, j'aurais peut-être dû négocier l'accompagnement sur une durée de 15 jours. La bonne nouvelle cependant est que pour cet accompagnement à la transition il n'y a qu'un engagement de moyen (en mode best-effort) et pas de résultat. No stress.

12 - Signature

Après plus de 60 échanges emails qui ont eu lieu avec l'acheteur sur 2 mois pour répondre aux questions et de nous aligner sur le contenu du contrat de vente, nous étions fin prêt à signer.

Parce que dans notre cas les documents de travails étaient tous des PDF, j'ai dû faire un suivi des changements... à l'ancienne :

13 - Réaliser le transfert des assets

Le transfert de tous les assets, la migration des applications et des accès ont été réalisé en quelques heures, sans downtime pour les utilisateurs.

14 - Attendre l'argent (rends l'argent !) et payer ses impôts

Pas la peine de faire un dessin sur cette dernière partie :).

Ainsi se termine ces 3 mois. "Qui ose gagne. Là où se trouve une volonté, il existe un chemin." disait Churchil.

Si tu lis jusqu'ici c'est sans doute que les SaaS, l'indiehacking, ou simplement le développement en général te passionne. J'ai une bonne nouvelle pour toi, il y a un slack pour ça, rejoins-nous ! Il est temps de mon côté de te laisser, les autres chapitres du livre NoBullshit Tech-Lead ne sortirons pas tout seul 😅 !

6/03/2020

FinOps - Reducing Google Cloud Storage costs

🤯 Inter-region network transfer can be a real PITA.

My latest Google-Cloud Invoice for my Image-Charts Saas was ~40% related with inter-region transfer. 💸💸

 - Why ? 🧐

 - I'm glad you asked 😍!

GCP billing report confirms that 40% came from Google Cloud Storage.

Drilling down I saw that the main costs were related with GCP "Storage egress between NA and EU" and "GCP Storage egress between EU and APAC".

Storage/sent bytes per location graph confirms it, I've sent more than 3TB of data from EU to Asia (APAC) & USA (NA) clusters.

WHYYYY 😭?

Because docker images 🐳 are stored on GCP EU (eu.gcr.io). And are downloaded nearly at every Kubernetes node auto-scaling-up steps. And Image-Charts scales. Like a lot.

I've updated @imagecharts continuous delivery pipeline yesterday to push images to the 3 locations (EU+Asia+USA) instead of one (EU) and I already see improvements 👍🔥

Conclusion: 20x Google Cloud Storage cost reduction!

5/19/2020

How to automatically activate PostgreSQL Row Level Security on tables with at least one policy attached

Row level security is an awesome feature that let you control how your database (PostgreSQL in my case) manage access to each row of a table based on some policies declared upfront. It's also really useful when you expose your database through a REST API with a gateway like PostgREST. I already talked enough about that :).

I often forgot to add the ALTER TABLE schema.table ENABLE ROW LEVEL SECURITY; statement when I declare row level security policies. Do you?

Let's use our database awesome introspection feature to list tables for which we attached access policies and then automatically activate row level security. The SQL request below list tables and display whether or not row level security is activated.

select pg_class.oid,
       pg_namespace.nspname || '.' || pg_class.relname as schema_table,
       pg_policy.polname as policy_name,
       pg_class.relrowsecurity as has_row_level_security_enabled
from pg_catalog.pg_policy
       inner join pg_catalog.pg_class on pg_class.oid = pg_policy.polrelid
       inner join pg_catalog.pg_namespace on pg_class.relnamespace = pg_namespace.oid;
oid schema_table policy_name has_row_level_security_enabled
369657 fsm.machine fsm_machines_access_policy true
369745 iam.user user_access_policy true
369803 actor.company company_access_policy true
369803 actor.company company_access_policy_for_update true
369842 contract_manager.contract contract_access_policy false

From the query output we observe that contract_manager.contract table does have an associated access policy called contract_access_policy but without row level security enabled on the table.

Let's now enable row level security for each table where at least one policy was defined:

update pg_catalog.pg_class
set relrowsecurity = true
where pg_class.oid in (select pg_class.oid
      from pg_catalog.pg_policy
      inner join pg_catalog.pg_class on pg_class.oid = pg_policy.polrelid
      where pg_class.relrowsecurity = false);

This is it! We've activated row level security (not in FORCE mode) to every table with attached policies. No more mistakes. No more boilerplate \o/

4/09/2020

PostgREST "response.headers guc must be a JSON array composed of objects with a single key and a string value" error

Yep. You are wondering why it's working locally and not in production right? Or maybe why PostgREST login feature is not working at all?

The response.headers guc must be a JSON array composed of objects with a single key and a string value is often related to the fact that some call were made to set a header field but the value to be set was empty. Take a look at your settings.secrets table. If it's empty, that's the problem.

At least on the PostgREST-starter-kit, the settings.secrets table should contains at least 2 rows:

jwt_secretyour_secret_here
jwt_lifetime 3600

Looking for some guidance on PostgREST? How to setup a CI/CD with it? What test strategy to use? As a CTO I've built multiple SaaS with it underneath and now help tech teams build fast, reliable and safer API with PostgREST and PostgreSQL. Hire me on CodeMentor or Malt!

3/03/2020

How to expose all public stored function in PostgREST/SubZero

DO $$
    DECLARE
        schema_name INFORMATION_SCHEMA.routines.routine_schema%TYPE = 'api';
        fns CURSOR FOR
            select routine_name from INFORMATION_SCHEMA.routines WHERE routine_schema = schema_name;
    BEGIN
        FOR fn_record IN fns LOOP
                EXECUTE 'grant execute on function ' || schema_name || '.' || fn_record.routine_name || ' to anonymous;';
            END LOOP;
    END$$;

Will expose all store functions from the api public schema in the generated swagger/openapi specification from PostgREST/SubZero. Of course, ensure that all underneath private tables have row-level-security enabled to stay secure.

How to expose all public views in PostgREST/SubZero

DO $$
    DECLARE
        schema_name INFORMATION_SCHEMA.views.table_schema%TYPE = 'api';
        views CURSOR FOR select table_name from INFORMATION_SCHEMA.views WHERE table_schema = schema_name;
    BEGIN
        FOR view_record IN views LOOP
                EXECUTE 'grant select, insert, update, delete on ' || schema_name || '.' || view_record.table_name || ' to anonymous;';
            END LOOP;
    END$$;

Will expose all views from the api public schema in the generated swagger/openapi specification from PostgREST/SubZero. Of course, ensure that all underneath private tables have row-level-security enabled.

2/26/2020

Validate an openapi or swagger API definition from a Gitlab-CI test step

Lets say you've built $BUILD_IMAGE container image at the build step. I did it on a NodeJS based project but it will work with other technology as well.

check-openapi-contract:
  stage: test
  retry: 1
  timeout: 15m
  script:
    - docker run --name=my-container -d -i -p 8080:8080 --rm $BUILD_IMAGE npm start
    - bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8080/swagger.json)" != "200" ]]; do sleep 5; done'
    - docker exec -i my-container curl http://localhost:8080/swagger.json -o ./swagger.json
    - docker exec -i my-container npx swagger-cli validate ./swagger.json

So what do we do? We start the server, then retrieve the swagger.json or openapi.json and leverage swagger-cli validate command to ensure our definition is valid and be notified if it is not. Nothing. More.

« »
 
 
Made with on a hot august night from an airplane the 19th of March 2017.