<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title></title>
		<description>My site</description>
		<link>https://obem.be</link>
		<atom:link href="https://obem.be/feed.xml" rel="self" type="application/rss+xml" />
		
			<item>
				<title>A new form of human interaction</title>
				<description>&lt;p&gt;Few days ago, Cloudflare published a &lt;a href=&quot;https://blog.cloudflare.com/perplexity-is-using-stealth-undeclared-crawlers-to-evade-website-no-crawl-directives&quot;&gt;blog post&lt;/a&gt; saying Perplexity is using undeclared crawlers to evade no-crawl directives. This sparked some interesting debates and I was particularly piqued by &lt;a href=&quot;https://x.com/illyism/status/1952368298370142571&quot;&gt;one&lt;/a&gt; arguing Perplexity is no different from a regular browser and should ignore no-crawl directives. As ridiculous as it initially sounded, I found the argument surprisingly valid and worth further thought. (Perplexity has since &lt;a href=&quot;https://x.com/perplexity_ai/status/1952531537385456019&quot;&gt;replied&lt;/a&gt; in the same defense).&lt;/p&gt;

&lt;h2 id=&quot;bots-crawlers-robotstxt-and-no-crawl&quot;&gt;Bots, Crawlers, robots.txt and no-crawl&lt;/h2&gt;
&lt;p&gt;Bots and crawlers are important elements of the web. These bots and crawlers (also called spiders) are automated programs that browse the internet without a human controlling them directly. (Crawlers are special types of bots that follow links and jump from one page to another).&lt;/p&gt;

&lt;p&gt;Search engines like Google use crawlers to discover and index web pages, so they can show them in search results. SEO tools use them to analyze how search engines see your websites. Monitoring tools use them to check if your site is up or down. And there are many more examples like price tracking, content aggregation, and social media auto-posting. But not all bots are good. Bots can also be used to do harmful things like spamming comments or forms, trying passwords on login pages, or for DDos attacks.&lt;/p&gt;

&lt;p&gt;To give website owners control of how bots interact with their site, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robots.txt&lt;/code&gt; was designed. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robots.txt&lt;/code&gt; file sits in the root directory of the website (example.com/robots.txt) and contains a directive on how bots should interact with the site—bots allowed/disallowed, pages to crawl, etc. In other words, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robots.txt&lt;/code&gt; says don’t crawl this site or this list of pages and the bots comply. But do they?&lt;/p&gt;

&lt;h2 id=&quot;browsers-vs-bots&quot;&gt;Browsers vs Bots&lt;/h2&gt;
&lt;p&gt;The argument “But Google Chrome browsers by users are not blocked, why is Perplexity different than a regular browser” falls apart by the single fact that browsers are fundamentally different from bots. Browsers are programs as well, but they are human-controlled programs (generally called &lt;strong&gt;clients&lt;/strong&gt;) designed for direct human interaction. This human interaction is a core component of the web that we have designed around:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Discussions in forums and boards&lt;/li&gt;
  &lt;li&gt;Commenting on blog posts&lt;/li&gt;
  &lt;li&gt;Interacting with ads&lt;/li&gt;
  &lt;li&gt;Recommending a post to a friend&lt;/li&gt;
  &lt;li&gt;Support via creator support platforms&lt;/li&gt;
  &lt;li&gt;Followers and likes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;clients-browsers-or-bots&quot;&gt;Clients: browsers or bots?&lt;/h2&gt;
&lt;p&gt;The way we interact with the web is rapidly changing. Human interaction that used to mean manually visiting a link and doing stuff on a web page can now be done with programmatic instructions to a [special type of] browser. Better still, tasks can now be performed by simply describing them in natural language: “open example.com and login with this username and password”. AI assistants interact in a similar way with webpages to answer specific real-time questions or process human requests like bookings.&lt;/p&gt;

&lt;p&gt;While we can argue that this is still different from manually visiting a browser to perform the tasks, we can agree it definitely blurs the line between what we define as “real” human interaction and not. Should this type of interaction follow &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robots.txt&lt;/code&gt; directives or ignore them like browsers? Should we have a new directive for this type of human-interaction? Should a website not allow me to place a food order or book air travel because I didn’t manually visit the page to place the request but did via a different form of client?&lt;/p&gt;

&lt;h2 id=&quot;it-is-a-lost-war&quot;&gt;It is a lost war&lt;/h2&gt;
&lt;p&gt;Blocking bots is at best a won battle but a lost war. For starters, there is no easy way to enforce &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robots.txt&lt;/code&gt; compliance or any non-client preventive rule. Unethical bots don’t follow them anyway. Overly strict methods like full-page captchas on content pages may work in the short term but will harm usability in the long run and ostracize that content from the web.&lt;/p&gt;

&lt;p&gt;I will repeat, “the way we interact with the web is rapidly changing”. I can’t remember the last time I visited Stack Overflow. Or Wikipedia. As web authors (owner, manager or whatever role you play), we need to redefine what we think human interaction is and redesign to accommodate the changes happening; designing expecting that the page will be accessed outside the standard human-visit methods we are familiar with.&lt;/p&gt;

&lt;p&gt;I get it. Human interaction on any web page is what converts to revenue—ads, subscriptions, audience, purchase. Losing this is the big fear (other fear of bots like spam have working or at least somewhat working answers). However it won’t get better. We need to start thinking of alternative ways to &lt;a href=&quot;https://x.com/cesifoti/status/1947717591776301216&quot;&gt;“fund” the web&lt;/a&gt;.&lt;/p&gt;
</description>
				<pubDate>Wed, 06 Aug 2025 00:00:00 +0000</pubDate>
				<link>https://obem.be/2025/08/06/a-new-form-of-human-interaction.html</link>
				<guid isPermaLink="true">https://obem.be/2025/08/06/a-new-form-of-human-interaction.html</guid>
			</item>
		
			<item>
				<title>On Prayer</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;“I still maintain my stand that prayers change nothing. If prayers need you to put your effort first to function, that means prayers do absolutely nothing; what is working is your efforts.” - &lt;a href=&quot;https://x.com/Allezamani/status/1931991116251123772&quot;&gt;X&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a genuine thought a lot of us have had at one point or another. These questions are good as they challenge what we think we know, what is true, and help us seek deeper understanding.&lt;/p&gt;

&lt;p&gt;Here is how I think about it:&lt;/p&gt;

&lt;h2 id=&quot;1&quot;&gt;1.&lt;/h2&gt;

&lt;p&gt;A good place to start is defining what prayer is. For a lot of us (Christians), we understand prayer  primarily as a channel of petition and request. This is part of it but if this is all you do in prayer, you will run out of things to ask and get bored. You can try it; I have.&lt;/p&gt;

&lt;p&gt;Prayer at its core, is fellowship. It is simply being present; like spending time with a friend, your kids or your partner. It is less about the ask and more about communion (Romans 8:26).&lt;/p&gt;

&lt;h2 id=&quot;2&quot;&gt;2.&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;So if request is part of prayer, meaning I can ask for things, why is effort needed?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;(The fact the effort is mentioned means this is a request that effort alone can fulfill—usually material things. When people ask questions like this, they are often referring to wealth).&lt;/p&gt;

&lt;p&gt;Close your eyes and imagine a world where all you need to do to get things is pray. I think it would be a world filled with religious facade and devoid of purpose or fulfillment. It wouldn’t be a good place.&lt;/p&gt;

&lt;p&gt;Prayer isn’t to bypass the human experience of effort and growth. That will be unfair of a just God. Perhaps the “answer” to prayer is often the strength or wisdom to engage with life’s challenges, rather than their removal.&lt;/p&gt;

&lt;p&gt;As humans we are limited. We get tired (Isaiah 40:30, 1 Kings 19:4). We make mistakes. Prayer gives strength (Psalm 138:3, Luke 22:43), peace (Philippians 4:7), and clarity (James 1:5, Jeremiah 33:3).&lt;/p&gt;

&lt;p&gt;By the way, I really doubt anyone can attribute all they have achieved in life to their sole effort—maybe most things, but definitely not all.&lt;/p&gt;

&lt;h2 id=&quot;3&quot;&gt;3.&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;So are you saying “Don’t pray for things”?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s ok to pray for things. We’ve all been in situations where we could use some help. I could really use a couple thousand pounds right now. But God doesn’t have pounds. He doesn’t spend them. The world’s value system is designed by man. So if I am praying for money, it has to come through this system—it won’t fall from heaven. What channel can this prayer be answered through? Have I invested in building strong relationships with friends &lt;a href=&quot;https://x.com/Ssaasquatch/status/1834617271022694634&quot;&gt;God could nudge&lt;/a&gt; to give me that? Have I worked on acquiring high-value skills that someone can pay me for?&lt;/p&gt;

&lt;p&gt;Miracles do happen but systems and principles govern the everyday life (John 6:26).&lt;/p&gt;

&lt;h2 id=&quot;4&quot;&gt;4.&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;What about the time I prayed for stuff (especially non-material things) and it didn’t happen?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Towards the end of December last year, J dreamt B was seriously beaten and she had to take her to the hospital. Before they got to the hospital, she woke up. She shared this with me and I nudged her to give B a call and check on her. Turned out B had been sick. J forced her to the hospital for a proper test and the doctor discovered there was fluid in one of her lungs. Some weeks and 3 operations later, a cancerous mass was discovered in the lung.&lt;/p&gt;

&lt;p&gt;This year, I prayed like I have never prayed before. And fasted. At least 30 minutes every morning and 1 hour every night, praying for a miracle. J was on another level. Besides our own sessions, she does at least another hour with her prayer group.&lt;/p&gt;

&lt;p&gt;Even when the doctor had a meeting with the family and close friends after her failed radiotheraphy and chemo, we still had faith. I mean, why did J had the dream in the first place?
One of those nights, I was literally just crying in prayer. If anything happened to B, we would be shattered.&lt;/p&gt;

&lt;p&gt;We lost her in April. And we were broken. B was the sister I never had. It was really devastating.&lt;/p&gt;

&lt;p&gt;I had many questions. What did we miss? Was it that we didn’t pray enough?&lt;/p&gt;

&lt;p&gt;There are many possible reasons we don’t get what we pray for. There are all those things you have heard—wrong kind of prayer, character development, faith building, etc. Sometimes, even our own actions sabotage our prayers. Personally, I try to focus on two things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Unbelief—a lack of that inner alignment and conviction&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; about what you are praying for. God can, but will he? Have I reached that place of conviction that he will?&lt;/li&gt;
  &lt;li&gt;God’s will (1 John 5:14) and sovereignty. As humans, our focus and interests limit our perspective. In the grand scheme of things, our desires may not align with God’s plan (Job 42:2–3).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I havent really been able to pray in the last 2 months. The few times I have managed to, all I pray for is strength. Some days are fine, but some evenings, I still see J cry.&lt;/p&gt;

&lt;p&gt;So yes, what did we miss? Why wasn’t she healed?&lt;/p&gt;

&lt;p&gt;Honestly, I don’t know. But as I try to find my way back, I find solace in two truths:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My role is to put in the effort (Titus 3:8) and confidently ask (1 John 5:14).&lt;/li&gt;
  &lt;li&gt;I can trust in His wisdom and plan to take care of the rest, accepting the outcome, whatever that is (Romans 8:28).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t have all the answers. These are just my own thoughts. Maybe they are wrong, maybe they don’t fully answer it, but I am curious to know what you think about prayer.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Dolapo Lawal has one of the best teaching I have heard on this—&lt;a href=&quot;https://www.youtube.com/watch?v=PekjiYOBAmI&quot;&gt;how to move mountains 1&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/watch?v=kC6RwLOX_j4&quot;&gt;2&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/watch?v=A0zIOeI0Fs0&quot;&gt;3&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
				<pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate>
				<link>https://obem.be/2025/06/11/on-prayer.html</link>
				<guid isPermaLink="true">https://obem.be/2025/06/11/on-prayer.html</guid>
			</item>
		
			<item>
				<title>Inflowdash - A dashboard and calendar of your upcoming revenue</title>
				<description>&lt;p&gt;I have been using an internal tool for some time now to check upcoming revenue for &lt;a href=&quot;https://engage.so/&quot;&gt;Engage&lt;/a&gt;. It connects to Stripe, pulls data for upcoming revenue and displays it in a calendar form. That’s it; nothing magical. This tool is now available at &lt;a href=&quot;https://inflowdash.com/&quot;&gt;inflowdash.com&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;why&quot;&gt;Why?&lt;/h2&gt;

&lt;p&gt;Current metrics dashboards are focused on data after the fact (after revenue is in). This is rightly so as they tell a story of growth. For inflowdash, it is the other way - data before the fact, which is perfect for forecasting. This helps:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Forecast and plan cash flow&lt;/li&gt;
  &lt;li&gt;Identify problems early&lt;/li&gt;
  &lt;li&gt;Manage churn and expansion revenue&lt;/li&gt;
  &lt;li&gt;Set accurate goals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do usage billing (like we do), where pricing is based on usage and not particularly fixed, this is even more accurate.&lt;/p&gt;

&lt;p&gt;So check it out: &lt;a href=&quot;https://inflowdash.com/&quot;&gt;inflowdash.com&lt;/a&gt;, share, and send me feedback: &lt;a href=&quot;https://twitter.com/kehers&quot;&gt;@kehers&lt;/a&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 06 May 2025 00:00:00 +0000</pubDate>
				<link>https://obem.be/2025/05/06/inflowdash.html</link>
				<guid isPermaLink="true">https://obem.be/2025/05/06/inflowdash.html</guid>
			</item>
		
			<item>
				<title>Email/Password Authentication Checklist</title>
				<description>&lt;p&gt;Here is a fairly rough checklist of things you should think of when building an email/password authentication system for a fairly sensitive application. This assumes the basics like securely hashing passwords, proper session management, and transmitting data over SSL are already covered. For a more comprehensive list, see the &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&quot;&gt;OWASP Cheat Sheet on Authentication&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ensure passwords are a minimum of 8 characters. Support all printable ASCII characters and spaces.&lt;/li&gt;
  &lt;li&gt;Implement two-factor authentication with an authenticator app at the minimum.&lt;/li&gt;
  &lt;li&gt;If your app allows multiple users, allow the admin to be able to “force” 2FA for all members.&lt;/li&gt;
  &lt;li&gt;Require password to update password or email.&lt;/li&gt;
  &lt;li&gt;Send an email notification on password update.&lt;/li&gt;
  &lt;li&gt;Send email notifications to both old and new email when email changes.&lt;/li&gt;
  &lt;li&gt;Lock account (could be for a specific period) after multiple failed login attempts. (OWASP recommends also logging the failed attempts)&lt;/li&gt;
  &lt;li&gt;Log authentication events with metadata like IP, client, and OS details.&lt;/li&gt;
  &lt;li&gt;If a user is accessing from a “new location”, and has no 2FA on account, send an email verification code. “New location” can be based on the client, IP, or a special fingerprint algorithm.&lt;/li&gt;
  &lt;li&gt;Throttle codes sent via email (or SMS) to the same user. You can use an exponential backoff strategy to increase delay between each message sent and have a limit over a specific period, e.g., maximum of 4 verification codes to the same user over a 1 hour period.&lt;/li&gt;
  &lt;li&gt;Allow users to be able to log out of all devices they are currently logged in on. This can be a button on their settings page. To implement this, you need to be able to map users to their session IDs and delete sessions by IDs. This may mean adding a DB store (Redis is a good choice) to your session layer.&lt;/li&gt;
  &lt;li&gt;When users change their password, log them out of all other devices they are currently logged in on. A more subtle implementation of this that I have seen makes this optional. A provided checkbox lets you decide if you want to or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I remember more, I will update the list.&lt;/p&gt;
</description>
				<pubDate>Tue, 17 Sep 2024 00:00:00 +0000</pubDate>
				<link>https://obem.be/2024/09/17/email-password-authentication-checklist.html</link>
				<guid isPermaLink="true">https://obem.be/2024/09/17/email-password-authentication-checklist.html</guid>
			</item>
		
			<item>
				<title>Serverless vs Kafka</title>
				<description>&lt;p&gt;I have been moving a lot of &lt;a href=&quot;https://engage.so/&quot;&gt;Engage’s&lt;/a&gt; architecture to Kafka of late. Even though the monolithic side has been winning the microservice vs monolithic debate in recent times, I am still a big fan of serverless microservices—using PaaS (we use Google Cloud) to host microservices. To be fair, what we do is more of a serverless architecture than a proper microservice architecture but our design goal aims to achieve the same as with a microservice architecture. This has helped us handle millions of events daily from the API and &lt;a href=&quot;https://engage.so/integrations&quot;&gt;integrations&lt;/a&gt; with less worry about scalability.&lt;/p&gt;

&lt;p&gt;So why move off PaaS? A couple of reasons. Top of it are two things.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Bringing together many separate services to make them compact—easier testing, faster interpolation of shared resources, more flexibility, and simpler deployment.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://serverlesshorrors.com/&quot;&gt;Predictive service cost&lt;/a&gt;. Note that this is not the same as being cheaper, although our current Kafka setup is, which is great. Being predictive is that I know I am going to pay just about the same amount whether or not I processed 50 million or 1 million events during the month.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But is the grass really greener on the Kafka side? Nope.&lt;/p&gt;

&lt;p&gt;There is the initial overhead of setting things up. (This is if you want to self-manage. You can get another PaaS that can do that for you. There are a couple of managed Kafka services). There is also your consumer design, which is a crucial part. You need to be able to design your consumers in a way that they can consume messages concurrently and efficiently. If for example, you have a topic with 100 partitions that is receiving messages at that throughput, or even more, then you need consumers processing at that speed as well. Now think of 10 topics of similar throughput. So what are you looking at? Threads? Multiple container instances? Node pools? With a serverless architecture, Google Cloud Function for example, you can specify a minimum and maximum number of instances to be deployed to handle such throughput and everything automatically gets taken care of for you. And that’s the beauty of serverless. You are less bothered about the abstraction.&lt;/p&gt;

&lt;p&gt;There are other things. Kafka doesn’t have any delay mechanism. Most serverless PaaS do. Amazon SQS has delays. Google Pub/Sub doesn’t but you can use Cloud Tasks.&lt;/p&gt;

&lt;p&gt;There is also monitoring and logging. Monitoring is how you know when something is wrong with Kafka or your consumers are failing. Logging is how you ensure messages are properly processed. I have found heartbeats monitoring a good way to monitor consumer uptime. For every consumer, register a heartbeat every time a message is processed. If no message is processed within a time frame (this will depend on the type of message the consumer processes), trigger an alert. (Betterstack has a &lt;a href=&quot;https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/&quot;&gt;good heartbeat monitoring&lt;/a&gt; service).&lt;/p&gt;

&lt;p&gt;All that said, things are holding up fine so far. Would our dependence on [serverless] PaaS be zero? I honestly doubt it.&lt;/p&gt;
</description>
				<pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate>
				<link>https://obem.be/2024/09/16/serverless-vs-kafka.html</link>
				<guid isPermaLink="true">https://obem.be/2024/09/16/serverless-vs-kafka.html</guid>
			</item>
		
			<item>
				<title>[Building Engage] #5. Working with unstructured data</title>
				<description>&lt;p&gt;A lot of content around data wrangling talks about dealing with structured data—data with a well defined schema. You can clearly define your database columns based on this schema to fit the incoming data. Most apps deal with this kind of data, so I understand. But for the other few, &lt;a href=&quot;https://engage.so/&quot;&gt;Engage&lt;/a&gt; for example, dealing with unstructured data is big work.&lt;/p&gt;

&lt;p&gt;I am not talking about storage here. Storing unstructured data is easy. If you are using an SQL db, you can normalize the unstructured data (JSON stringify for example) in a column. If you are using a NoSQL db, even better. MongoDB for example supports schemaless data by design. Just dump your data in JSON style and you are good. But how do you store the data in a way that is easy to “search” the different fields? How do you sort on any of the fields? What about aggregation?&lt;/p&gt;

&lt;p&gt;At Engage, users can track custom user attributes, events and properties. This means, each business have unique user data points and can do things like segment users based on those data points.&lt;/p&gt;

&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/qZzA_NS4Ar8?si=esN2saKhZqXan106&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;Consequently, this means in total, the different user attribute fields from all businesses can be in hundreds, thousands, and eventually millions. Let me explain properly. Business A can have the following user attributes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;name&lt;/li&gt;
  &lt;li&gt;gender&lt;/li&gt;
  &lt;li&gt;location&lt;/li&gt;
  &lt;li&gt;avatar&lt;/li&gt;
  &lt;li&gt;city&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Business B:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;name&lt;/li&gt;
  &lt;li&gt;age&lt;/li&gt;
  &lt;li&gt;website&lt;/li&gt;
  &lt;li&gt;occupation&lt;/li&gt;
  &lt;li&gt;address&lt;/li&gt;
  &lt;li&gt;rating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time you take that across a thousand businesses, 4 unique user attributes per one, that’s about 4,000 unique attributes. How for example do you store this in a way that the business can easily do a search like “Find all users that live in Lagos”? 4k unique columns that are indexed? Then do this for user events data. And events properties.&lt;/p&gt;

&lt;p&gt;Unfortunately, this is not an indepth post to explore this. I hope to be able to gather more conversations, posts, and talks about this. But here are some pointers based on our experience.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Find a database suited for your need. What you should be looking at are databases that are specifically designed for logging. I know you are probably looking for a recommendation here but business needs varies. This sounds easy but it’s not. You will have to experiment with different options and finally decide on what really works for you. That is the hard work.&lt;/li&gt;
  &lt;li&gt;You can actually store “all” the unique fields using Elasticsearch’s dynamic mapping. The fields are indexed and you can perform search and aggregations on them. Well, maybe not all. There is a setting that limits this to 1k fields which of course you can change. But remember, the more the fields, the more degradation on performance and resources.&lt;/li&gt;
  &lt;li&gt;“&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-explosion.html&quot;&gt;Mapping explosion&lt;/a&gt;” happens when the number of these mapped fields in Elasticsearch is too high. One trick to prevent hitting it is to create multiple versions of the same index and route each query to the right index. So for example, you can cluster every account into a specific group (I love using the 16 hex digits: 0-9 and a-f). Suffix your index with the group, e.g, “logs” become “logs-a”, “logs-b”, etc. Requests goes to the specific index for the account making the request. What this means is that each of these indices would have 1k fields (so 16k altogether). The performance impact is way better than using a single index (say “logs”) and increasing the mapping limit to 16k.&lt;/li&gt;
  &lt;li&gt;ClickHouse has JSON fields that allow you store unstructured data as JSON objects. It also has JSON functions to extract data from these fields and manipulate them. PostHog has a &lt;a href=&quot;https://posthog.com/handbook/engineering/clickhouse/working-with-json&quot;&gt;post&lt;/a&gt; about working with this. Personally, I found the &lt;a href=&quot;https://clickhouse.com/docs/en/integrations/data-formats/json#using-pairwise-arrays&quot;&gt;Pairwise Arrays&lt;/a&gt; option more performant so it’s something you can also explore. (Read the whole post about &lt;a href=&quot;https://clickhouse.com/docs/en/integrations/data-formats/json&quot;&gt;Working with JSON in ClickHouse&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would love to hear about your own experiences, what has worked for you, talks, posts, videos and everything about working with unstructured data that you have seen. I will try and do more detailed posts about working with unstructured data as well.&lt;/p&gt;

&lt;p&gt;I am &lt;a href=&quot;https://twitter.com/kehers&quot;&gt;@kehers&lt;/a&gt; on Twitter (or X as it is now called).&lt;/p&gt;
</description>
				<pubDate>Wed, 14 Feb 2024 00:00:00 +0000</pubDate>
				<link>https://obem.be/2024/02/14/working-with-unstructured-data.html</link>
				<guid isPermaLink="true">https://obem.be/2024/02/14/working-with-unstructured-data.html</guid>
			</item>
		
			<item>
				<title>2023: Reset</title>
				<description>&lt;p&gt;2022 was rough. The year started with lots of energy - a new baby, Engage growing, and then new realities started kicking in. I watched Engage’s runway drain as we struggled with reaching product market fit. We tried many ideas from the playbook - inbound sales, community, socials, and ads, but conversion growth was slow.&lt;/p&gt;

&lt;p&gt;The reasonable choice was to raise a new round but I didn’t want to get distracted from the product. I knew what effort went into raising the first round and how distracting and stressful it could be. That coupled with our slow growth, I decided against it. I kept convincing myself we would be able to figure it out and be profitable before we ran out of funds. You guessed right, I was wrong.&lt;/p&gt;

&lt;p&gt;But we kept pushing. I had stopped paying myself way earlier and was really struggling. And piling on debts. At this point, it was obvious the kind of growth I was waiting for to justify our next raise wasn’t going to happen that fast so I started engaging my investors about raising a new round. Then my dad died in August.&lt;/p&gt;

&lt;p&gt;I felt numb. Then pain. Not from the loss. But that I didn’t get closure on a lot of things I wanted to. Then anger.&lt;/p&gt;

&lt;p&gt;Towards the end of the year, a lucky break finally happened. I got a windfall from a secondary equity sale. I was able to settle my debts and loaned a big chunk to Engage to extend our runway while I continued my engagement with my investors.&lt;/p&gt;

&lt;p&gt;2023 started with lethargy. I kept thinking about money, growth, raising, what could change, and everything in between. On the home front, there was peace. Even when everything around was crumbling, that, and my faith were my solace. I would wake up every morning, look at the family, and get the energy to face the day.&lt;/p&gt;

&lt;p&gt;In February, I got wind of Tech Nation shutting down. My friend had been pressing me for over 3 years to apply but I would shrug it off. This time, it felt the timing was right. I needed that reset, especially for the family. So I prayed and applied. A bit over a week later, I got a yes.&lt;/p&gt;

&lt;p&gt;The relocating experience wasn’t as easy as I thought it would be. But that is a totally different post I need to take time to write in detail. I traveled to the UK in May for about a month to explore and get accommodation. By the end of June, myself and the family relocated to the UK.&lt;/p&gt;

&lt;p&gt;In May and the following months, I suffered serious burnout. At first, I thought it was because I was away from family and support systems, especially with faith. But even after the family relocated, it took some effort to get back in shape. It was strange. I was happy about the move, although not ecstatic (I don’t get ecstatic about a lot of things). I was happy for a lot of things – settling in a place with proper and better systems; where I do not have to worry about low-level problems like electricity or water. But more importantly, seeing my daughter jump ecstatically when we took her to her new school and the new radiance on my wife’s face brought me lots of joy and made it worth it.&lt;/p&gt;

&lt;p&gt;At this point, Engage was at &lt;a href=&quot;https://www.paulgraham.com/pinch.html&quot;&gt;the fatal pinch&lt;/a&gt;. Raising in 2023 was another level of hard. And I failed at trying to do so. By Q3, it came down to 2 options: increase revenue and/or decrease spend. Increasing revenue wasn’t something that could be forced, so I started with the other option. I had a call with the team and explained the situation to them. I had to let people go and drop expenses in many other ways. Fortunately, by Q4, revenue, was enough to cover expenses.&lt;/p&gt;

&lt;p&gt;Things are calmer now. My wife started a new job and I am adjusting to the reality of the new environment, culture, and life. Of course, I am still worried about money. I don’t have a proper source of income yet and living expenses are more expensive here. But I am focused on the positives. It was a crazy year, but a lot of things went right. Every morning, the first thing on my mind is gratitude for grace, privilege, family, and how far we have come. I have seen the future. It’s beautiful. I choose to focus on that.&lt;/p&gt;
</description>
				<pubDate>Fri, 29 Dec 2023 00:00:00 +0000</pubDate>
				<link>https://obem.be/2023/12/29/2023-reset.html</link>
				<guid isPermaLink="true">https://obem.be/2023/12/29/2023-reset.html</guid>
			</item>
		
			<item>
				<title>[Building Engage] #4. Avatars</title>
				<description>&lt;p&gt;Features like avatars look trivial in products with user profiles, but most times, they are not. (Is anything actually trivial?).  It’s even more interesting, as with &lt;a href=&quot;https://engage.so/?utm_source=obembe&amp;amp;utm_medium=blog&amp;amp;utm_campaign=building-engage&quot;&gt;Engage&lt;/a&gt;, when users can’t upload their profile pictures themselves. Where do we get one from?&lt;/p&gt;

&lt;h2 id=&quot;gravatar&quot;&gt;Gravatar!&lt;/h2&gt;

&lt;p&gt;If you are familiar with this, this would be your answer as well. &lt;a href=&quot;https://gravatar.com/&quot;&gt;Gravatar&lt;/a&gt; is one of the oldest forms of getting people’s profile pictures. Make a request with the person’s [encrypted] email address as a parameter and get a profile picture in return.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const hash = md5(email)
return `&amp;lt;img src=&quot;https://www.gravatar.com/avatar/${hash}&quot;&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There are two major drawbacks though.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Gravatar uses email address as the unique identifier. This means if you don’t have the user’s email (a product that doesn’t collect user’s email address for example) there is no way you can get the user’s profile picture.&lt;/li&gt;
  &lt;li&gt;The user needs to have set up that email on Gravatar. While a lot of people have their emails set up on Gravatar, some people do not. This means having a user’s email does not automatically guarantee that they will have a profile picture on Gravatar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;defaultfallback-image&quot;&gt;Default/fallback image&lt;/h2&gt;

&lt;p&gt;Gravatar provides a solution to the second challenge – if the profile does not exist. You can provide a fallback image (as a URL) that is displayed instead.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const hash = md5(email)
const fallback = encodeURIComponent(&apos;https://my.app/images/default.png&apos;)
return `&amp;lt;img src=&quot;https://www.gravatar.com/avatar/${hash}?d=${fallback}&quot;&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is good. But it means every profile with no gravatar will have the same default image. If you are Facebook or Twitter (X), that may be fine. For us, I wanted some uniqueness. Gravatar has a way to generate unique defaults but I am not a fan of the designs. Are there alternative avatar services that can be used instead of Gravatar or as a fallback for Gravatar? A lot. But what are we looking for to start with?&lt;/p&gt;

&lt;h2 id=&quot;service-checklist&quot;&gt;Service Checklist&lt;/h2&gt;

&lt;p&gt;I created a checklist of what we want in a service.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;It should be able to use email as an identifier, i.e., pull the avatar attached to the user’s email.&lt;/li&gt;
  &lt;li&gt;Emails are optional user attributes on Engage, so if it doesn’t exist, the name (first or last name) should be used. Usernames would have been the next best thing but usernames are not standard attributes on Engage. Names can’t be used to pull a profile picture but we can use it to create an avatar of initials.&lt;/li&gt;
  &lt;li&gt;If the user has an email but it doesn’t return a profile picture, get initials from the email and use that to create an avatar.&lt;/li&gt;
  &lt;li&gt;If the profile has no names (names are optional as well), generate a unique random avatar using the user’s ID. We do not want a default profile picture for all users with no email or name.&lt;/li&gt;
&lt;/ol&gt;

&lt;video width=&quot;100%&quot; controls=&quot;&quot;&gt;
  &lt;source src=&quot;/assets/image/2023/10/avatar.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;

&lt;h2 id=&quot;unavatar&quot;&gt;Unavatar&lt;/h2&gt;

&lt;p&gt;I love &lt;a href=&quot;https://unavatar.io&quot;&gt;Unavatar&lt;/a&gt;! And for many reasons. It’s open source, has simple implementation, and pulls profile pictures from &lt;a href=&quot;https://unavatar.io/#/?id=providers&quot;&gt;many places&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Just append the user’s username, email, or domain to unavatar.io and that’s it&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;img src=&quot;https://unavatar.io/engage.so&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unavatar checks preference 1. It doesn’t check preference 2 but has a fallback option so we can get an initials avatar service as fallback. It doesn’t check preference 3 as well but we can use a different service as a fallback if the user has no name.&lt;/p&gt;

&lt;h2 id=&quot;vercel-avatar&quot;&gt;Vercel Avatar&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt; has an &lt;a href=&quot;https://github.com/vercel/avatar&quot;&gt;avatar service&lt;/a&gt; that generates beautiful gradients and can generate initials from a text parameter. We can’t use it to pull profile images using emails as reference but it is a perfect fallback for other services because it can generate a unique avatar from an ID and also generate a unique avatar with initials.&lt;/p&gt;

&lt;p&gt;Generating a unique [gradient] avatar from the user’s ID works well as fallback for Gravatar and Unavatar but initials avatar does not work for Gravatar. The unique avatar can be generated in PNG format (default) but the initials avatar has to be SVG. Gravatar does not support SVG as a fallback image.&lt;/p&gt;

&lt;h2 id=&quot;tiley&quot;&gt;Tiley&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.getoutline.com/&quot;&gt;Outline&lt;/a&gt; has &lt;a href=&quot;https://github.com/outline/tiley/tree/main&quot;&gt;Tiley&lt;/a&gt;, an avatar service that solves the SVG problem. We can generate an initials avatar in PNG, JPG, or even SVG format.&lt;/p&gt;

&lt;p&gt;Tiley can’t generate a unique avatar without the initials. And if we don’t want to go with Vercel Avatar as a fallback for unique avatars when we don’t have the user’s email or name, then we can use another service for unique avatars.&lt;/p&gt;

&lt;h2 id=&quot;boring-avatar&quot;&gt;Boring Avatar&lt;/h2&gt;

&lt;p&gt;My recent favorite is &lt;a href=&quot;https://boringavatars.com/&quot;&gt;Boring Avatars&lt;/a&gt;. I love the styles and there is a &lt;a href=&quot;https://github.com/boringdesigners/boring-avatars-service/blob/main/README.md&quot;&gt;documentation&lt;/a&gt; on how to use it as a service.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;img src=&quot;https://source.boringavatars.com/${variant}/${size}/${id}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;what-should-i-use&quot;&gt;What should I use?&lt;/h2&gt;

&lt;p&gt;As with everything, it depends. What user attributes do you have? Email? Username? Name? Do you mind the same default image for all users? Or do you want something dynamic? What design style? Can users upload their profile pictures themselves?&lt;/p&gt;

&lt;p&gt;In the end, we went with Boring Avatar for profiles without email or name and Gravatar for profiles with email or name with Tiley as a backup for initials. Earlier versions of Engage used Unavatar and it will always be my first choice. However, even though I doubt we would ever hit it, I was [subconsciously] conscious the hosted version is rate-limited and I didn’t want to self-host yet another service.&lt;/p&gt;
</description>
				<pubDate>Sun, 08 Oct 2023 00:00:00 +0000</pubDate>
				<link>https://obem.be/2023/10/08/avatars.html</link>
				<guid isPermaLink="true">https://obem.be/2023/10/08/avatars.html</guid>
			</item>
		
			<item>
				<title>Re: Passkeys</title>
				<description>&lt;p&gt;“Passkeys are a new, passwordless authentication method that offer a convenient authentication experience for sites and apps, using just a fingerprint, face scan or other screen lock.” (&lt;a href=&quot;https://security.googleblog.com/2023/05/making-authentication-faster-than-ever.html&quot;&gt;source&lt;/a&gt;). Don’t think too much about how it works, you can check out the demo at &lt;a href=&quot;http://passkeys.io&quot;&gt;passkeys.io&lt;/a&gt;. I just tested it myself and couldn’t help but &lt;a href=&quot;https://twitter.com/kehers/status/1662744844345786368&quot;&gt;comment on the ease&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I love this – anything that replaces password authentication actually, but in the end, ease (of use and technical implementation) will determine its adoption and consequently, if this will totally replace passwords.&lt;/p&gt;

&lt;p&gt;Google claims it’s easier to use but I’d argue “easier” is relative. It feels technical experiencing it for the first time.&lt;/p&gt;

&lt;p&gt;Below is a signup process (implementations will vary but it’s typically going to be close to this):&lt;/p&gt;

&lt;p&gt;Step 1 - create an account with your username/email&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/image/2023/05/step1.png&quot; alt=&quot;Sign up process - create an account&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Step 2 - set a passkey&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/image/2023/05/step2.png&quot; alt=&quot;Sign up process - save a passkey&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For a non-technical person, Step 2 may be confusing. Or at least cause a pause. I actually paused to understand what exactly was going on there. One question I’d assume the user will be asking at that point is, “What am I supposed to do here?”&lt;/p&gt;

&lt;p&gt;If I save the passkey on the device, logins are actually easier.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/image/2023/05/auth.png&quot; alt=&quot;Authentication on same device&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I click “Continue”, I am requested to unlock my laptop and I am logged in. But what happens if I don’t have any lock on the device? I tested this on my phone and I couldn’t create a passkey or log in without a lock on the device itself. This makes sense as it ensures access to your unlocked device does not mean access to your logins.&lt;/p&gt;

&lt;p&gt;The experience of logging in on a device when you have a passkey on another device is not what I will call easy as well. There is no “easy” way to connect to the passkey saved on another device. Below is trying to log in on mobile while my passkey is on my laptop.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/image/2023/05/mobile-login.jpeg&quot; alt=&quot;Authentication on a different device&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Looking at the friction of these options, give me password authentication please.&lt;/p&gt;

&lt;p&gt;Passkey is a good improvement compared to other passwordless methods: multi-device authentication compared to WebAuthn and less friction (not to mention delivery delays and failures) compared to one-time codes/links. I am keenly looking forward to what adoption will look like for this one.&lt;/p&gt;
</description>
				<pubDate>Sun, 28 May 2023 00:00:00 +0000</pubDate>
				<link>https://obem.be/2023/05/28/re-passkeys.html</link>
				<guid isPermaLink="true">https://obem.be/2023/05/28/re-passkeys.html</guid>
			</item>
		
			<item>
				<title>[Building Engage] #3. Fighting spam</title>
				<description>&lt;p&gt;If you give people any tool for engagement, expect it to be used for spam. This is worse for marketing tools. There are always bad actors looking to exploit this to send phishing emails and spam to people.&lt;/p&gt;

&lt;p&gt;For Engage, the earliest fail-safe we placed was to not provide email sending infrastructure ourselves but allow customers connect their existing infrastructure. To Engage, you had to connect your ESP (email service provider). (We support Mailgun, AWS, Sparkpost and Sendgrid). This solved the issue on two fronts:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;These services have processes in place to ensure you are genuine. For you to have a production-ready AWS SES account, for example, you needed to have &lt;a href=&quot;https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html&quot;&gt;made a valid argument&lt;/a&gt; for your use case and successfully passed the review process.&lt;/li&gt;
  &lt;li&gt;These services track your use according to policy limits and penalize when necessary. They actively track things like your bounce rates and complaint rates and if these go above particular thresholds, your account gets disabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But there was a problem with this experience. Since our primary targets are product marketers/managers, telling them to connect an ESP before they get value on the product creates friction. They probably don’t know what ESP they use or care enough (yet) to connect the ESP. And not everyone wants to connect their ESP anyway. To reduce the path to value as much as possible, we had to automatically provision a default way to send outbound emails without the need for customers to connect an ESP. And we were back at solving the spam issue ourselves.&lt;/p&gt;

&lt;p&gt;There are different approaches to solving spam depending on what you are building. For marketing tools, the general approach is to limit what customers can do, review the account and whitelist. There are slight variations of this approach but the general concept remains the same. This was the same approach we took.&lt;/p&gt;

&lt;p&gt;Our original plan was to limit to a particular allowed daily sends (50? 100? 10?). Customers can then request full access. One other approach we considered was starting accounts in sandbox mode where we only allow sends to a whitelisted list of emails (emails need to be verified to be added). To move to production, a request is made and we review. This is the common pattern with transactional ESPs. But it adds another layer of friction and complexity that we would rather not include. In the end, we decided to completely limit email sends for new accounts. Customers can explore and use other features 100% but email sending (both broadcast and automation) is disabled till we manually review.&lt;/p&gt;

&lt;p&gt;The drawback with the approach is that we need to review every single signup (and quickly too). But that’s not a bad thing. It lets us know our customers. At scale, we may not be able to manually do it fast as necessary but there are ways to solve that, e.g. automated checks, machine learning flagging.&lt;/p&gt;

&lt;p&gt;But signup is only the beginning. Fighting spam is continuous work. After signup and whitelisting, it is important to continually monitor and ensure people use the service responsibly. Bounces and complaints need to be regularly monitored to ensure it falls within an acceptable range. I particular love how &lt;a href=&quot;https://www.intercom.com/help/en/articles/567399-intercom-s-workspace-review-process&quot;&gt;Intercom&lt;/a&gt; also checks links in outbound messages for web threats and this is something we have also adopted.&lt;/p&gt;
</description>
				<pubDate>Tue, 22 Mar 2022 00:00:00 +0000</pubDate>
				<link>https://obem.be/2022/03/22/fighting-spam.html</link>
				<guid isPermaLink="true">https://obem.be/2022/03/22/fighting-spam.html</guid>
			</item>
		
	</channel>
</rss>
