diff --git a/aleksis/apps/kort/migrations/0002_card_printer.py b/aleksis/apps/kort/migrations/0002_card_printer.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb3a6dea60bf735781e5fc44670ae9dea4d5d653
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0002_card_printer.py
@@ -0,0 +1,52 @@
+# Generated by Django 3.2.9 on 2021-11-30 20:07
+
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('kort', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='card',
+            name='print_finished_at',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Printed at'),
+        ),
+        migrations.AddField(
+            model_name='card',
+            name='print_started_at',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Printed at'),
+        ),
+        migrations.CreateModel(
+            name='CardPrinter',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=255, verbose_name='Name')),
+                ('description', models.TextField(blank=True, verbose_name='Description')),
+                ('location', models.CharField(max_length=255, verbose_name='Location')),
+                ('status', models.CharField(choices=[('online', 'Online'), ('offline', 'Offline'), ('with_errors', 'With errors')], max_length=255, verbose_name='Status')),
+                ('status_text', models.TextField(blank=True, verbose_name='Status text')),
+                ('last_seen_at', models.DateTimeField(blank=True, null=True, verbose_name='Last seen at')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+            ],
+            options={
+                'verbose_name': 'Card printer',
+                'verbose_name_plural': 'Card printers',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddField(
+            model_name='card',
+            name='printed_with',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='kort.cardprinter', verbose_name='Printed with'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index d3409873aa2bd2cc6f91cc2221d890779e8430b3..7fd4b6ebbd505af42119b31ed14127587acc264f 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -5,6 +5,37 @@ from aleksis.core.mixins import ExtensibleModel
 from aleksis.core.models import Person
 
 
+class CardPrinterStatus(models.TextChoices):
+    ONLINE = "online", _("Online")
+    OFFLINE = "offline", _("Offline")
+    WITH_ERRORS = "with_errors", _("With errors")
+
+
+class PrintStatus(models.TextChoices):
+    REGISTERED = "registered", _("Registered")
+    IN_PROGRESS = "in_progress", _("In progress")
+    FINISHED = "finished", _("Finished")
+
+
+class CardPrinter(ExtensibleModel):
+    name = models.CharField(max_length=255, verbose_name=_("Name"))
+    description = models.TextField(verbose_name=_("Description"), blank=True)
+    location = models.CharField(max_length=255, verbose_name=_("Location"))
+
+    status = models.CharField(
+        max_length=255, verbose_name=_("Status"), choices=CardPrinterStatus.choices
+    )
+    status_text = models.TextField(verbose_name=_("Status text"), blank=True)
+    last_seen_at = models.DateTimeField(verbose_name=_("Last seen at"), blank=True, null=True)
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = _("Card printer")
+        verbose_name_plural = _("Card printers")
+
+
 class Card(ExtensibleModel):
     person = models.ForeignKey(
         Person, models.CASCADE, verbose_name=_("Person"), related_name="cards"
@@ -13,6 +44,26 @@ class Card(ExtensibleModel):
     valid_until = models.DateField(verbose_name=_("Valid until"))
     deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)
 
+    # Print status
+    printed_with = models.ForeignKey(
+        CardPrinter,
+        on_delete=models.SET_NULL,
+        blank=True,
+        null=True,
+        verbose_name=_("Printed with"),
+    )
+    print_started_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
+    print_finished_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
+
+    @property
+    def print_status(self) -> PrintStatus:
+        if self.print_finished_at:
+            return PrintStatus.FINISHED
+        elif self.print_started_at:
+            return PrintStatus.IN_PROGRESS
+        else:
+            return PrintStatus.REGISTERED
+
     class Meta:
         verbose_name = _("Card")
         verbose_name_plural = _("Cards")