Updates

Announcing Public Beta

Announcing Public Beta

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.


What's next

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:

  • Invoicing - Deliver the first version of our invoicing service
  • Budget - Add support for budgeting
  • Apps & Integrations - Integrate with Xero, Fortnox, and QuickBooks
  • Bug Fixes & Improvements - General improvements to the platform

ProductHunt

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.


Get started

Now that Middays is in public beta, feel free to try us out by signing in here.

Security

Incident report for June 22

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.


Incident timeline

  • On June 22nd, at 3:01 PM, we got notified about the issue.
  • On June 22nd, at 3:44 PM, we identified the issue.
  • On June 22nd, at 3:49 PM, we resolved the issue.

How this affects you

No user action is required to continue safely.
Accessed: User email and name.


Actions and remediations

These are the preventative measures that we have already taken:

  • Fixed our misconfigured access policy

These are the preventative measures that we will be taking immediately:

  • Improving monitoring and alerting

Conclusion

We sincerely apologize to everyone affected by this incident, and we appreciate your understanding.

Please reach out to us at support@midday.ai if you have any questions.

Updates

Midday Engine

Midday Engine

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 wich 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.


Engine - Providers


Architecture

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.



Cloudflare

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.


Hono

We chose to use the Hono framework because it's portable, super lightweight, and provides an incredible developer experience among other features:

  • Caching - we cache directly in Cloudflare
  • Zod - for parsing and validating payloads
  • OpenAPI - generate our specification

Typesense

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.


Unkey

We will soon offer Engine to other companies to use. We will use Unkey for API key management, rate limits, and analytics.


Trigger.dev

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:

  • Retries
  • Logging
  • Error handling

Baselime

To deliver a great service, it's super important to fix bugs and issues fast. With the help of Baselime, 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.

  • Logging and debugging
  • Alerts
  • Performance metrics

OpenStatus

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.

  • Status page
  • Alerts

What's next

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.

Updates

Introducing Midday Assistant

Introducing Midday Assistant

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:


  • Find the things you are looking for (transactions, invoices, documents, and more)
  • Watch your spending
  • View your profit, revenue, burn rate, and revenue graphs
  • Find transactions without receipts

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.

Engineering

How we are sending notifications easy with Novu

How we are sending notifications easy with Novu

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.


The challenge: Implementing a unified infrastructure for notifications

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.


The solution: Novu

While we tried a bunch of different providers we decided to go with Novu based on our requirements:

  • Support Resend, Expo
  • In App notifications
  • User Preferences

Novu not only ticked every box they also got a huge bonus for being open source and you get 30K events/month for free.


Implementation and SDKs

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.


Novu - Variables


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,
  },
});

The results: Beautiful In App Notificaitons and Emails


Midday - In App Notifications


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.


Midday - In App Notifications


The future of Midday x Novu

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.


GitHub

While this is just a draft on how we are using Novu in production you can dig deeper into the codebase in our repository.

Engineering

How we take data-driven decisions with LogSnag

How we take data-driven decisions with LogSnag

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.


LogSnag - Events


How we implemented LogSnag

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.


LogSnag - Events


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.

Engineering

How we implemented link-sharing using Dub.co

How we implemented link-sharing using Dub.co

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.


How we implemented sharing for our reports


Midday - Overview


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.

Engineering

Why Midday trusts Resend to send financial notifications

Why Midday trusts Resend to send financial notifications

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

Engineering

How we are using Trigger.dev for our background jobs

How we are using Trigger.dev for our background jobs

At Midday, we use background jobs extensively. Here's an in-depth exploration of our approach.


Transactions Setup

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.


Transactions Sync

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.


Process Document

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.


Match Inbox

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.


Export Transactions

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.


GitHub

You can find all of our jobs in our GitHub repository.

Update

The Early Adopter Plan

The Early Adopter Plan

Being an Early Adopter

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.


The Early Adopter Plan

  • This plan includes access to all features.
  • Priced at $30 per month but free while in beta.
  • Your subscription price will remain unchanged for life as a token of appreciation for supporting Midday in its early stage, regardless of the additional features we introduce.

Community-Driven Development

As we continue to enhance Midday, we highly value your input and ideas. Here are three ways you can share your thoughts with us:

Stress free by midday.

An all-in-one tool for freelancers, contractors, consultants, and micro
businesses to manage their finances, track projects, store files, and send invoices.