Midday x ElevenLabs

Together with ElevenLabs, we brought together the Stockholm tech scene on the Founders House rooftop to talk tech, enjoy some pizza, and share ideas.
Together with ElevenLabs, we brought together the Stockholm tech scene on the Founders House rooftop to talk tech, enjoy some pizza, and share ideas.
First off, thank you for an incredible 2024. Since launching our public beta in May, we’ve onboarded over 12,000 businesses something we’re incredibly proud of.
We’ve been working tirelessly to bring all the features we’ve promised but we’re just getting started. Here’s what’s coming next:
We’ve been moving fast and that comes with a cost. Our focus now is on refining and improving key areas.
Midday currently integrates with three bank providers. We’re adding two more; Enable Banking for Europe, and Pluggy in Brazil. This will bring more stability and expand coverage across the EU and Brazil.
We’re improving transaction matching with receipts and invoices, making the inbox feature even smarter. Over time it will learn from your patterns ensuring greater accuracy. Plus we’re rolling out UI updates for a smoother experience.
Thanks to your feedback we have a prioritized list of bugs and refinements that we’ve already started tackling. Keep the feedback coming!
We have big plans for mobile ensuring you can stay on top of your business anytime anywhere with key insights and actionable tools. Viktor is working on bringing the design to life. Here’s a sneak peek more details soon.
Our AI assistant is evolving. It will include memory and learn from your workflows making it more action-driven by suggesting tasks before you even ask. This also lays the foundation for our upcoming mobile app.
Many of you have asked for more ways to integrate Midday into your workflows. We’ve partnered with Speakeasy to offer SDKs in TypeScript, Python and Go along with expanded API access.
We’re making Midday available in multiple languages across the entire platform so you can work in the language that suits you best. To make this seamless we’re using Languine our own AI-powered localization tool ensuring accurate and dynamic translations.
We’re aiming to be out of beta in two months and can’t wait to bring Midday to even more businesses worldwide.
Once again thank you for all your support and feedback. Midday is shaped by you. Feel free to reply to this email we read every message.
I’m incredibly excited to announce that our most requested feature is finally here—Invoicing! Let’s dive into how it works and what we’re focusing on for the next month.
We have designed and developed a new visual editor from the ground up, based on extensive research and feedback from early users. While it may look simple, it’s incredibly powerful for all your needs.
Here are some features we support from the start:
And much more!
You can choose to send an email to your customers with the newly created invoice. This will open a web-based invoice where they can communicate in real-time instead of exchanging emails back and forth.
They can also download a PDF, and as the owner, you can see the last view date.
If you have a bank account connected to Midday, we will automatically mark an invoice as paid when we find a match. You’ll be notified, and the generated PDF will be attached to your transaction for bookkeeping.
You’ll also be notified when invoices become overdue, allowing you to send a reminder email with the press of a button.
Our main focus right now is to prepare Midday for our v1 release in Q1 next year. We’re working on our banking integrations to handle various edge cases more effectively, ironing out all the small bugs, and improving the overall experience with Midday.
If you find any bugs or have any feedback, just let us know!
Create your first invoice and let us know what you think get started here.
With our upcoming Invoicing feature, we have explored various methods to generate PDF invoices, ranging from running Puppeteer to using a headless browser on Cloudflare, as well as considering paid services and generating PDFs using React.
We've noticed that generating PDF invoices is a common challenge in the developer community, so we decided to share our solution with you.
We are building a new experience for invoices in Midday. You'll be able to create and send invoices to your customers, as well as generate PDFs for each invoice.
Our interface is highly customizable with a visual editor where you can easily change the layout, add your logo, and tailor the text to your preferences.
We use an editor based on Tiptap to support rich text, with AI-powered grammar checking and text improvement available at the click of a button.
While the editor saves the content using JSON, we also needed to ensure compatibility with our PDF generation process.
When you send an invoice to a customer, they will receive an email with a unique link to the invoice. Clicking this link will render the invoice on a web page where you and the customer can communicate in real-time using our chat interface.
You'll also be able to track when the customer has viewed the invoice and if they have any questions about it.
There are numerous ways to generate PDFs, and we've explored many different solutions. While we considered paid services, we ultimately decided to prioritize giving you full control over the invoices without relying on external services.
We went with react-pdf
to generate the PDFs. This is a great library that allows us to generate PDFs using React. We can easily customize the layout and add our own styles to the documents and it feels just like react-email
concept where we use react to generate our templates.
The invoice is then generated and saved to your Vault, so we can match it to incoming transactions and mark it as paid.
We first create an API endpoint that will generate the PDF and return the PDF as Content Type application/pdf
.
import { InvoiceTemplate, renderToStream } from "@midday/invoice";
import { getInvoiceQuery } from "@midday/supabase/queries";
import { createClient } from "@midday/supabase/server";
import type { NextRequest } from "next/server";
export const preferredRegion = ["fra1", "sfo1", "iad1"];
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
const supabase = createClient();
const requestUrl = new URL(req.url);
const id = requestUrl.searchParams.get("id");
const size = requestUrl.searchParams.get("size") as "letter" | "a4";
const preview = requestUrl.searchParams.get("preview") === "true";
if (!id) {
return new Response("No invoice id provided", { status: 400 });
}
const { data } = await getInvoiceQuery(supabase, id);
if (!data) {
return new Response("Invoice not found", { status: 404 });
}
const stream = await renderToStream(await InvoiceTemplate({ ...data, size }));
const blob = await new Response(stream).blob();
const headers: Record<string, string> = {
"Content-Type": "application/pdf",
"Cache-Control": "no-store, max-age=0",
};
if (!preview) {
headers["Content-Disposition"] =
`attachment; filename="${data.invoice_number}.pdf"`;
}
return new Response(blob, { headers });
}
With this approach we can also add ?preview=true
to the URL to generate the PDF in the browser without downloading it. This is useful for previewing the invoice before generating the PDF.
And here is the template for the invoice, we register a custom font, generate a QR code and making sections and formatting the invoice.
You can find the full code for the invoice template here.
import { Document, Font, Image, Page, Text, View } from "@react-pdf/renderer";
import QRCodeUtil from "qrcode";
import { EditorContent } from "../components/editor-content";
import { LineItems } from "../components/line-items";
import { Meta } from "../components/meta";
import { Note } from "../components/note";
import { PaymentDetails } from "../components/payment-details";
import { QRCode } from "../components/qr-code";
import { Summary } from "../components/summary";
const CDN_URL = "https://cdn.midday.ai";
Font.register({
family: "GeistMono",
fonts: [
{
src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Regular.ttf`,
fontWeight: 400,
},
{
src: `${CDN_URL}/fonts/GeistMono/ttf/GeistMono-Medium.ttf`,
fontWeight: 500,
},
],
});
export async function InvoiceTemplate({
invoice_number,
issue_date,
due_date,
template,
line_items,
customer_details,
from_details,
payment_details,
note_details,
currency,
vat,
tax,
amount,
size = "letter",
link,
}: Props) {
const qrCode = await QRCodeUtil.toDataURL(
link,
{
width: 40 * 3,
height: 40 * 3,
margin: 0,
},
);
return (
<Document>
<Page
size={size.toUpperCase() as "LETTER" | "A4"}
style={{
padding: 20,
backgroundColor: "#fff",
fontFamily: "GeistMono",
color: "#000",
}}
>
<View style={{ marginBottom: 20 }}>
{template?.logo_url && (
<Image
src={template.logo_url}
style={{
width: 78,
height: 78,
}}
/>
)}
</View>
<Meta
invoiceNoLabel={template.invoice_no_label}
issueDateLabel={template.issue_date_label}
dueDateLabel={template.due_date_label}
invoiceNo={invoice_number}
issueDate={issue_date}
dueDate={due_date}
/>
<View style={{ flexDirection: "row" }}>
<View style={{ flex: 1, marginRight: 10 }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ fontSize: 9, fontWeight: 500 }}>
{template.from_label}
</Text>
<EditorContent content={from_details} />
</View>
</View>
<View style={{ flex: 1, marginLeft: 10 }}>
<View style={{ marginBottom: 20 }}>
<Text style={{ fontSize: 9, fontWeight: 500 }}>
{template.customer_label}
</Text>
<EditorContent content={customer_details} />
</View>
</View>
</View>
<LineItems
lineItems={line_items}
currency={currency}
descriptionLabel={template.description_label}
quantityLabel={template.quantity_label}
priceLabel={template.price_label}
totalLabel={template.total_label}
/>
<Summary
amount={amount}
tax={tax}
vat={vat}
currency={currency}
totalLabel={template.total_label}
taxLabel={template.tax_label}
vatLabel={template.vat_label}
/>
<View
style={{
flex: 1,
flexDirection: "column",
justifyContent: "flex-end",
}}
>
<View style={{ flexDirection: "row" }}>
<View style={{ flex: 1, marginRight: 10 }}>
<PaymentDetails
content={payment_details}
paymentLabel={template.payment_label}
/>
<QRCode data={qrCode} />
</View>
<View style={{ flex: 1, marginLeft: 10 }}>
<Note content={note_details} noteLabel={template.note_label} />
</View>
</View>
</View>
</Page>
</Document>
);
}
We will be launching the Invoicing feature soon to our early access users, let us know if you want to be included in the early access program to get all the new features as soon as they are ready.
We would love to hear what you think about the Invoicing feature, and we would love to hear from you if you have any ideas or feedback for the feature.
Sign up for an account and start using Midday today.
We are excited to announce the launch of Apps in Midday. You can now easily connect your favorite tools to streamline your workflow. Starting with Slack, we will continue to expand to other tools that you love.
There is a new Apps menu in the sidebar. Here you can find all the apps available to you.
To install an app, you can either click the "Install" button, or click the "Details" button to learn more about the app.
You can read more about the specific app in the details page, in this example we are looking at the Slack app that will enable you to connect your Slack workspace to Midday and get notifications about new transactions and give you Midday Assistant right in your Slack workspace.
In the example above we are using the Slack Assistant to send a message to the channel. You can use the Slack Assistant to know your burn rate, track your expenses, and get insights about your transactions.
You can also upload receipts and invoices to Midday right from Slack and we will extract the data and match it to your transactions.
While this is just the beginning, we are excited to bring more apps to you.
Sign up for an account and start using Midday today.
In this technical deep dive, we’ll explore how we built the Midday Slack Assistant by leveraging the Vercel AI SDK, Trigger.dev, and Supabase. Our goal was to create an AI-powered assistant that helps users gain financial insights, upload receipts, and manage invoices—all seamlessly from within their Slack workspace.
Midday already has an assistant available on both web and desktop via Electron, but we haven’t yet built a dedicated mobile app. As we continue iterating on our web app, we plan to eventually create a mobile version using Expo. In the meantime, we wanted to give our users a way to interact with Midday on the go. With that in mind, we turned to Slack, where users can quickly upload receipts and invoices without leaving the app they’re already using for work.
We aimed to enable our users to:
Slack recently introduced a new messaging experience for app agents and assistants, making them more discoverable and accessible in user workflows. This update allows developers to build AI assistants based on their own APIs, seamlessly integrated into the Slack interface.
When users install our Slack app from the Apps section within Midday, we obtain the necessary permissions to interact with the Slack API, send messages, and listen to events.
To connect the Slack app to a user’s workspace, we utilize Slack’s OAuth flow to retrieve an access token with the required scopes. These scopes include:
assistant_thread_context_changed
: Detects changes in the assistant thread context.assistant_thread_started
: Tracks when an assistant thread starts.file_created
: Monitors file uploads.message.channel
: Captures messages posted in public channels.message.im
: Captures direct messages.Once authenticated, we store this data in our apps
table along with the team ID and access token, structured as a JSONB column for flexibility.
Here’s where the magic happens: We use a Next.js route to handle incoming Slack events and route them to our internal handler. This handler checks the event payload to determine the appropriate action. If it detects a file upload, it triggers a background job via Trigger.dev. If it’s a message, we forward it to our Assistant logic.
When a file upload event occurs, we send the file to a background job and save it in our Supabase database. Using OCR, we extract key details such as amount
, currency
, date
, and merchant
from the receipt. Then, we attempt to match the data with existing transactions in our system using Azure Document Intelligence Models.
Here’s a quick demo of how the file upload works in Slack:
When you open the Slack Assistant, you’re greeted with a welcome message and a set of suggested actions. This welcome message is triggered by the assistant_thread_started
event and looks like this:
import type { AssistantThreadStartedEvent, WebClient } from "@slack/web-api";
export async function assistantThreadStarted(
event: AssistantThreadStartedEvent,
client: WebClient,
) {
const prompts = [
{ title: "What’s my runway?", message: "What's my runway?" },
// Additional prompts...
];
try {
await client.chat.postMessage({
channel: event.assistant_thread.channel_id,
thread_ts: event.assistant_thread.thread_ts,
text: "Welcome! I'm your financial assistant. Here are some suggestions:",
});
await client.assistant.threads.setSuggestedPrompts({
channel_id: event.assistant_thread.channel_id,
thread_ts: event.assistant_thread.thread_ts,
prompts: prompts.sort(() => 0.5 - Math.random()).slice(0, 4),
});
} catch (error) {
console.error("Error in assistant thread:", error);
await client.assistant.threads.setStatus({
channel_id: event.assistant_thread.channel_id,
thread_ts: event.assistant_thread.thread_ts,
status: "Something went wrong",
});
}
}
When the user selects one of the suggested prompts, we trigger the assistant_thread_message
event. Based on the message, we then use the appropriate tool with the Vercel AI SDK.
To generate the response, we use the generateText
function with the OpenAI model. This approach makes it easy to switch between different models and frameworks as needed.
It's crucial to use the thread_ts
parameter to ensure the response is sent back to the same thread, rather than creating a new one.
We also handle the request using waitUntil
to ensure Slack receives a 200 response within 3 seconds. This prevents Slack from retrying the request, which we want to avoid.
Here is the code for the assistant thread message:
import { openai } from "@ai-sdk/openai";
import { createClient } from "@midday/supabase/server";
import { generateText } from "ai";
import { startOfMonth, subMonths } from "date-fns";
import { getRunwayTool, systemPrompt } from "../../tools";
export async function assistantThreadMessage(
event: AssistantThreadStartedEvent,
client: WebClient,
{ teamId }: { teamId: string },
) {
const supabase = createClient({ admin: true });
await client.assistant.threads.setStatus({
channel_id: event.channel,
thread_ts: event.thread_ts,
status: "Is thinking...",
});
const threadHistory = await client.conversations.replies({
channel: event.channel,
ts: event.thread_ts,
limit: 5,
inclusive: true,
});
const lastTwoMessages = threadHistory.messages
?.map((msg) => ({ role: msg.bot_id ? "assistant" : "user", content: msg.text || "" }))
.reverse();
const { text } = await generateText({
model: openai("gpt-4o-mini"),
maxToolRoundtrips: 5,
system: systemPrompt,
messages: [
...(lastTwoMessages ?? []),
{ role: "user", content: event.text },
],
tools: { getRunway: getRunwayTool({ defaultValues, supabase, teamId }) },
});
if (text) {
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts,
blocks: [{ type: "section", text: { type: "mrkdwn", text } }],
});
} else {
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts,
text: "Sorry, I couldn't find an answer to that.",
});
await client.assistant.threads.setStatus({
channel_id: event.channel,
thread_ts: event.thread_ts,
status: "",
});
}
}
Midday is fully open-source, and you can find the pull request for the Slack Assistant and Apps here.
You can aslo try Midday for your own business by signing up here.
In this release, we're introducing three major updates along with a few helpful additions. You'll enjoy a much better vault experience with search and filters, document classifications, and significantly improved speed. Plus, we've added AI filters for transactions and a much better import feature.
We have rebuilt our Vault to be much faster, added a search where you can search for content within your documents, plus we have added the option for you to enable classifications of documents, for example invoices and receipts, so it's easy to filter.
We extract information and make your documents easy to find by simply searching for what you are looking for.
With the help of AI, we can classify your documents for an even easier way to find and identify them. You can enable this feature in your Vault settings.
We have updated our import solution to support CSV, PDF, and screenshots of transactions to help as many users as possible get started with Midday.
We have updated our UI to support manual override on mapping fields when we can't figure them out for you.
We added support for uploading images of transactions to assist users whose banks don't have CSV files available.
You can also manually create transactions from the transactions page.
It's now easier than ever to find the transaction you are looking for, or filter based on a specific date range. For example, you can search for: "Transactions from Q1 this year without receipts". You get what I mean - give it a try and let us know what you think.
A much cleaner look, context-based filters, and the ability to navigate them using your keyboard for fast access.
Sometimes, you want to change many transactions at the same time. We have now updated the UI to be much better for this purpose.
Our time tracker is getting a much-needed UI update that's much easier to understand and navigate. Along with that, our native app will also get a timer and command menu.
Viktor is working hard on the design and prototyping extensively to create the best possible experience with creating invoices. We will update you all along the way, and we are really excited to deliver this feature to all of you soon!
Give these a try and let us know what you think get started here.
In this release, we’re introducing three major updates along with a few helpful additions. You’ll enjoy a much better inbox experience with improved company names, logos, and bulk upload using drag-and-drop. Plus, our Assistant has received several upgrades.
We have rebuilt our Inbox to extract information more reliably and quickly. You can now bulk upload using drag-and-drop, and we will reconcile these with your transactions.
Easily upload multiple files at once with our new drag-and-drop feature.
Experience higher accuracy in identifying names, amounts, and logos.
We have updated our assistant to be nearly 30% faster thanks to GPT-4o and several performance improvements in our data layer.
You can now create reports directly from the Assistant.
Upload documents and ask questions around them.
We are exploring new ways to interact with your Assistant using voice commands.
Thanks to our new engine, you can now search for your bank directly. It's much more reliable, and you can view the status of your connection. You can also manually sync your connection in the Settings.
It's now possible to just search for your bank.
Sometimes, the connection to your bank may change. We now clearly display this in the Settings and provide instructions on how to resolve it, either through a sync or a reconnection.
Especially for banks in the EU, you need to reconnect your bank every 3-6 months. We now send a reminder email and provide an easy flow that takes just 60 seconds to get you back on track.
Our time tracker will soon includes real-time tracking capabilities, allowing you to monitor productivity and manage your time more effectively.
We have started developing our invoice feature and have created designs based on feedback from many of you to ensure the best possible experience.
Soon, you will gain a clear picture of your business finances, even if you operate with multiple currencies.
Give these a try and let us know what you think get started here.
For the past three months, we have been working hard in private beta with our customers. Today, we are excited to announce our public beta.
Both Viktor and I want to personally thank all of you who have given us feedback, attended meetings, and supported us during this time. We are super excited to continue our journey together to make Midday the best all-in-one tool for running our business smarter.
As we enter our public beta, new customers will have access to a special deal of $49/month, but it will be free during the beta period.
We aim to have Midday v1
ready in October, which is also when we will start charging for the service.
Until then, here are our focus areas:
Along with this milestone, we are launching on Product Hunt on Wednesday, June 26 (12:00 am PST). We would love to have your support by voting for us. You can subscribe for a notification here.
Now that Middays is in public beta, feel free to try us out by signing in here.
On June 22nd, we got notified by Lunchcat that users' email addresses and names could be accessed through our API.
We have addressed the issue and revised our security policy accordingly.
We deeply apologize for any impact this incident may have had on our users. Our commitment to transparency and prevention remains steadfast. Below is a summary of what occurred, how we resolved the issue, and the measures we are implementing to prevent future occurrences.
No user action is required to continue safely.
Accessed: User email and name.
These are the preventative measures that we have already taken:
These are the preventative measures that we will be taking immediately:
We sincerely apologize to everyone affected by this incident, and we appreciate your understanding.
Please reach out to us at [email protected] if you have any questions.
The core of Midday is based on our customers transactions. Using this data, we can do a lot to help you run your business smarter when it comes to financial insights, reconciling receipts, and more.
Today we support 3 different banking providers which enables us to connect to over 20 000+ banks in 33 countries across US, Canada, UK and Europe and more to come.
Here is an overview of the technologies and services we use to build our engine.
We provide an API that gives our services transformed transactions, accounts, and balances for Midday to use. Here is how we built it and the technologies we chose.
We use a lot of services in Cloudflare. Workers is the compute where our API is hosted, using Hono as our framework.
The API uses OpenAPI v3.0 and exposes endpoints for transactions, accounts, search, and balances.
We use Cloudflare KV for session caching with GoCardless to avoid hitting any rate limits and for general caching purposes.
In the end, we have one interface for our customers to search for their institution. To support this, we retrieve all institutions from our providers, transform the information, and store it in Typesense. We use a simple Cron job in Cloudflare every 24 hours to ensure we have the most recent information.
To connect to Teller, you need to use mTLS to authenticate. Thanks to Cloudflare, we can just upload our certificate and configure our worker to use it when needed.
We chose to use the Hono framework because it's portable, super lightweight, and provides an incredible developer experience among other features:
For our customers, we want just one interface for them to search and find their bank. For that to be possible, we needed an index, and we went with Typesense because it's super powerful and easy to use.
We show banks based on country, fuzzy search, and priority.
We will soon offer Engine to other companies to use. We will use Unkey for API key management, rate limits, and analytics.
Our platform fetches transactions regularly with the help of background jobs hosted by Trigger.dev, communicating with our Engine API via RPC. We also use:
To deliver a great service, it's super important to fix bugs and issues fast. With the help of Sentry, we can see what's going on and where we have issues. Additionally, we can also know when external APIs underperform or if we have any other bottlenecks.
Because we rely on external APIs, it's important for us to monitor their statuses. In our Engine, we expose a /health endpoint that requests each service’s endpoints so we can use OpenStatus to monitor our Engine's health.
Based on this status, we can exclude providers that have temporary issues and get a single source of truth about how our Engine works.
We are currently updating our engine. In the end, other companies will be able to use our engine to support a wider range of banks with just one API in their systems.
If you are interested, feel free to sign up here for more information.
Midday is fully open source. Feel free to check out our code here.
Our most requested feature is now ready for you to try! Introducing Midday Assistant. With help from the community, we have implemented many great features. Here are some highlights of what you can do using the assistant today:
These are just some examples. You can ask follow-up questions and dig deeper into your data.
This is just the beginning of Midday Assistant, and we will continue to invest and listen to your feedback so you have everything you need to run your business. Don't hesitate to give us feedback directly from the assistant or by scheduling a quick 15-minute call with us to share your thoughts!
If you are running Midday on Mac, you should update to v1.9
for the latest version.
To get started you can sign in here.
Midday is all about being on top of your business. Stay informed about your business insights, track when you get paid, receive updates on new transactions, and access general information how your business actually are doing. And that includes well-timed notifications via emails and push notifications, carefully curated for your convenience.
From the beginning, we knew the importance of a unified approach to managing our notifications. From past experiences of stitching together different solutions to support various notification needs, we were determined to create a system that could seamlessly support web, desktop, and mobile applications. Our goal is to ensure that our notification system can scale alongside our growth.
While we tried a bunch of different providers we decided to go with Novu based on our requirements:
Novu not only ticked every box they also got a huge bonus for being open source and you get 30K events/month for free.
We started with created our own package @midday/notifications
where
we installed @novu/node
dependency, where we register our notification
types for convenience:
// Our notification templates
export enum Templates {
Transaction = "transaction",
Transactions = "transactions",
Inbox = "inbox",
Match = "match",
}
// Our notification types (email|in-app)
export enum Events {
TransactionNewInApp = "transaction_new_in_app",
TransactionsNewInApp = "transactions_new_in_app",
TransactionNewEmail = "transaction_new_email",
InboxNewInApp = "inbox_new_in_app",
MatchNewInApp = "match_in_app",
}
And using our New Transactions email as an example, which is sent from a background job using Trigger.dev when you have new transactions.
import { Notifications, Types, Events } from "@midday/notifications";
// Generate html from react-email
const html = await renderAsync(
TransactionsEmail({
fullName: user.full_name,
transactions,
locale: user.locale,
})
);
await Notifications.trigger({
name: Events.TransactionNewEmail,
payload: {
type: Types.Transaction,
subject: t("transactions.subject"),
html,
},
user: {
subscriberId: user.id,
teamId: team_id,
email: user.email,
fullName: user.full_name,
avatarUrl: user.avatar_url,
},
});
Because we are sending the email with the variables subject
and html
with the generated content we are just adding those to Novu and then we're done.
And to send In App Notifications it's just a matter of changing the Events
and
Types
.
import { Notifications, Types, Events } from "@midday/notifications";
await Notifications.trigger({
name: Events.TransactionNewInApp,
payload: {
recordId: transaction.id,
type: Types.Transaction,
description: t("notifications.transaction", {
from: transaction.name,
}),
},
user: {
subscriberId: user.id,
teamId: team_id,
email: user.email,
fullName: user.full_name,
avatarUrl: user.avatar_url,
},
});
Thanks to the SDK @novu/headless
we implemented the notification center for Midday
in a matter of hours to match our branding exacly like we wanted while Novu delivers
the notifications in realtime.
And for user preferences we just use their API together with a simple server action.
We are really happy that we chose to use Novu, We have several smart notifications planned that will offer valuable business insights. Plus, Novu will integrate smoothly with our upcoming mobile app, allowing us to send push notifications effortlessly via Expo by just connecting a new provider.
While this is just a draft on how we are using Novu in production you can dig deeper into the codebase in our repository.
It's important for every new product to take the right decisions as often you can, and thanks for our public roadmap, votes on feature request and building in public we have a deep connection with our users, but on top of that we also want data to back our decisions. And thats why we have implemented LogSnag to track a lot of events so we can take data-driven decisions too.
There are planty of ways how you can implement analytics, in this blog post we will share how we solved it in Midday using NextJS and server-actions.
Because we have a monorepo we started with creating a new package called @midday/events
where we install @logsnag/next
.
The package includes all the events we want to track in Midday for example:
{
SignIn: {
name: "User Signed In",
icon: "🌝",
channel: "login",
},
SignOut: {
name: "User Signed Out",
icon: "🌝",
channel: "login",
},
}
And based of these realtime events we can make clear graphs like Line Chart, Bar Chart and Funnel Charts.
When you sign in to Midday we first ask you about tracking, we want you to keep your privacy. This is done by showing a Toast
component with the option to Accept or Decline, we save this decision
in a cookie so we now if we should add a identifier for the events or not.
We run a server action called tracking-consent-action.ts
:
"use server";
import { Cookies } from "@/utils/constants";
import { addYears } from "date-fns";
import { cookies } from "next/headers";
import { action } from "./safe-action";
import { trackingConsentSchema } from "./schema";
export const trackingConsentAction = action(
trackingConsentSchema,
async (value) => {
cookies().set({
name: Cookies.TrackingConsent,
value: value ? "1" : "0",
expires: addYears(new Date(), 1),
});
return value;
}
);
We then wrap the track
method from LogSnag to enable or disabled the user_id
to the event.
export const setupLogSnag = async (options?: Props) => {
const { userId, fullName } = options ?? {};
const consent = cookies().get(Cookies.TrackingConsent)?.value === "0";
const logsnag = new LogSnag({
token: process.env.LOGSNAG_PRIVATE_TOKEN!,
project: process.env.NEXT_PUBLIC_LOGSNAG_PROJECT!,
disableTracking: Boolean(process.env.NEXT_PUBLIC_LOGSNAG_DISABLED!),
});
if (consent && userId && fullName) {
await logsnag.identify({
user_id: userId,
properties: {
name: fullName,
},
});
}
return {
...logsnag,
track: (options: TrackOptions) =>
logsnag.track({
...options,
user_id: consent ? userId : undefined,
}),
};
};
We use the setupLogSnag
function like this:
export const exportTransactionsAction = action(
exportTransactionsSchema,
async (transactionIds) => {
const user = await getUser();
const event = await client.sendEvent({
name: Events.TRANSACTIONS_EXPORT,
payload: {
transactionIds,
teamId: user.data.team_id,
locale: user.data.locale,
},
});
const logsnag = await setupLogSnag({
userId: user.data.id,
fullName: user.data.full_name,
});
logsnag.track({
event: LogEvents.ExportTransactions.name,
icon: LogEvents.ExportTransactions.icon,
channel: LogEvents.ExportTransactions.channel,
});
return event;
}
);
We have a lot of events and charts pushing us to take right decisions, you can find the source code for this in our repository here.
Earlier this week Dub.co shared our customer story on why we choose Dub as our link-sharing infrastructure.
In this blog post where gonna share a little more in details how we implemented this functionality.
We have some features like Time Tracker, Reports and files from Vault that our users shares outside their company and with that we have an authorization layer on our side using Supabase, but these links are often really long because they include a unique token.
Our solution was to implement Dub to generate unique short URLs.
If you look closely you can se our link looks like this: https://go.midday.ai/5eYKrmV
When the user clicks Share
we execute a server action using the library next-safe-action
that looks like this:
const createReport = useAction(createReportAction, {
onError: () => {
toast({
duration: 2500,
variant: "error",
title: "Something went wrong pleaase try again.",
});
},
onSuccess: (data) => {
setOpen(false);
const { id } = toast({
title: "Report published",
description: "Your report is ready to share.",
variant: "success",
footer: (
<div className="mt-4 space-x-2 flex w-full">
<CopyInput
value={data.short_link}
className="border-[#2C2C2C] w-full"
/>
<Link href={data.short_link} onClick={()=> dismiss(id)}>
<Button>View</Button>
</Link>
</div>
),
});
},
});
The nice thing with next-safe-action is that you get callbacks on onError and onSuccess so in this case we show a toast based on the callback.
The action is pretty straightforward too, we first save the report based on the current parameters (from, to and type) depending on what kind of report we are creating.
We save it in Supabase and get a id back that we use to generate our sharable URL.
const dub = new Dub({ projectSlug: "midday" });
export const createReportAction = action(schema, async (params) => {
const supabase = createClient();
const user = await getUser();
const { data } = await supabase
.from("reports")
.insert({
team_id: user.data.team_id,
from: params.from,
to: params.to,
type: params.type,
expire_at: params.expiresAt,
})
.select("*")
.single();
const link = await dub.links.create({
url: `${params.baseUrl}/report/${data.id}`,
rewrite: true,
expiresAt: params.expiresAt,
});
const { data: linkData } = await supabase
.from("reports")
.update({
link_id: link.id,
short_link: link.shortLink,
})
.eq("id", data.id)
.select("*")
.single();
const logsnag = await setupLogSnag();
logsnag.track({
event: LogEvents.OverviewReport.name,
icon: LogEvents.OverviewReport.icon,
channel: LogEvents.OverviewReport.channel,
});
return linkData;
});
With the combination of server actions, Supabase and Dub we can create really beautiful URLs with analytics on top.
You can find the source code for this in our repository here.
From sign up, to implementing the SDK, Resend was different - we were setup in less than an hour.
Here at Midday, we care deeply about design, and we knew from the beginning that we wanted branded emails with reusable components.
By integrating react.email with Tailwind CSS, we've crafted an optimal user experience. This combination has been transformative, allowing us to enable our local development and online hosting.
The transition to Resend was strikingly smooth. I'm used to other services where you need to request access, quotes, and other information before getting started. From sign up, to implementing the SDK, Resend was different - we were setup in less than an hour.
We use Resend to ensure that our clients receive notifications for new bank transactions, invitations, and other significant interactions. What truly sets Resend apart for us are the continuous changes and improvements, like the Batch API, which specifically helped optimize our invite system.
I didn't think that you could further innovate an email API, but Resend keeps shipping features that we did not know we needed.
We haven't had the need to contact Resend for support yet, but the team has supported us while we shared our journey building emails on X, which truly showed me how they differ from other email providers in the best way.
I'm really excited for our launch, along side my co-founder Viktor Hofte.
Resend is a game changer when it comes to developer experience and design. If you care deeply about your product you should use Resend.
This is a cross post from Resend
At Midday, we use background jobs extensively. Here's an in-depth exploration of our approach.
Upon successful user authentication of their bank, we initiate the creation of a bank_connection
containing essential data and provider information (Plaid, GoCardLess, Teller). This facilitates subsequent retrieval of transactions on their behalf. Additionally, we store bank connection accounts in bank_accounts
, along with an enable/disable flag, enabling us to determine which accounts to fetch transactions from.
Once bank_connection
and bank_accounts
are securely stored in the database, we trigger the transactions-setup
job. This job orchestrates the initial synchronization from the providers through batch requests, ensuring resilience against extensive transaction loads. Moreover, it dynamically schedules intervals for running transactions-sync
every hour for each team. Clients subscribe to the eventId
to track the completion of the initial sync process.
The transactions-sync
task fetches transactions for each team hourly. A team may have multiple connected bank accounts, requiring one request per account. We aim to consolidate new transactions into a single collection for notification purposes. Transactions are limited to the last 30 days, streamlining data retrieval.
This approach offers a 30-day window for rectifying any potential errors and acquiring new transactions. Moreover, teams can seamlessly enable/disable accounts between sync runs, as transactions are fetched exclusively for enabled accounts. If a team_id
is not found, indicating deletion, the scheduler is promptly removed.
Upon receipt of a new inbound email from Postmarks, we upload the attachments (invoices) to Supabase and create an inbox
record. Subsequently, we trigger the process-document
job, responsible for parsing the invoice using Google Document AI to extract vital information such as due_date
, amount
, and currency. Successful extraction triggers the match-inbox
event.
The match-inbox
process utilizes the inboxId
, teamId
, and amount
to verify transactions matching the invoice amount, accounting for sign differences (transactions are always signed, whereas invoices are not). This verification extends back 45 days, considering cases where no attachments are available yet. Currently, encountering multiple matches necessitates further validation, a feature we plan to address in future UI enhancements.
The export-transactions
task retrieves selected transactionIds
, gathers their attachments, compiles a CSV file, and uploads it to our Vault. Clients subscribe to monitor the progress status of this operation.
You can find all of our jobs in our GitHub repository.
We are currently collaborating with our first group of beta users to refine our product and find the right fit for the market. If you're interested in shaping the future of Midday and becoming an early adopter, feel free to join our community. There's no obligation to contribute, and we're thrilled that you want to try out the system.
During our private beta phase, you may encounter some bugs, but we genuinely want all your feedback.
As we continue to enhance Midday, we highly value your input and ideas. Here are three ways you can share your thoughts with us: